mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-10-31 22:40:01 +00:00
[feature] Allow users to create + delete bookbarks, and view bookmarked statuses (#1168)
* Implement Bookmarks * Update based on review comments * Update swagger doc * Fix argument passing to status.Bookmark * Update changed test * Updates based on latest PR review
This commit is contained in:
parent
e58d2d8122
commit
477ae50933
26 changed files with 1230 additions and 5 deletions
|
@ -152,8 +152,8 @@ Crossed out - will not be implemented / will be stubbed only.
|
||||||
- [ ] /api/v1/statuses/:id/unmute POST (Unmute notifications on a status)
|
- [ ] /api/v1/statuses/:id/unmute POST (Unmute notifications on a status)
|
||||||
- [ ] /api/v1/statuses/:id/pin POST (Pin a status to profile)
|
- [ ] /api/v1/statuses/:id/pin POST (Pin a status to profile)
|
||||||
- [ ] /api/v1/statuses/:id/unpin POST (Unpin a status from profile)
|
- [ ] /api/v1/statuses/:id/unpin POST (Unpin a status from profile)
|
||||||
- ~~/api/v1/statuses/:id/bookmark POST (Bookmark a status)~~
|
- [x] /api/v1/statuses/:id/bookmark POST (Bookmark a status)
|
||||||
- ~~/api/v1/statuses/:id/unbookmark POST (Undo a bookmark)~~
|
- [x] /api/v1/statuses/:id/unbookmark POST (Undo a bookmark)
|
||||||
- [x] Media
|
- [x] Media
|
||||||
- [x] /api/v1/media POST (Upload a media attachment)
|
- [x] /api/v1/media POST (Upload a media attachment)
|
||||||
- [x] /api/v1/media/:id GET (Get a media attachment)
|
- [x] /api/v1/media/:id GET (Get a media attachment)
|
||||||
|
@ -242,8 +242,8 @@ Crossed out - will not be implemented / will be stubbed only.
|
||||||
- ~~Suggestions~~
|
- ~~Suggestions~~
|
||||||
- ~~/api/v1/suggestions GET (Get suggested accounts to follow)~~
|
- ~~/api/v1/suggestions GET (Get suggested accounts to follow)~~
|
||||||
- ~~/api/v1/suggestions/:account_id DELETE (Delete a suggestion)~~
|
- ~~/api/v1/suggestions/:account_id DELETE (Delete a suggestion)~~
|
||||||
- ~~Bookmarks~~
|
- [x] Bookmarks
|
||||||
- ~~/api/v1/bookmarks GET (See bookmarked statuses)~~
|
- [x] /api/v1/bookmarks GET (See bookmarked statuses)
|
||||||
|
|
||||||
### Non-API tasks
|
### Non-API tasks
|
||||||
|
|
||||||
|
|
|
@ -3326,6 +3326,34 @@ paths:
|
||||||
summary: Get an array of accounts that requesting account has blocked.
|
summary: Get an array of accounts that requesting account has blocked.
|
||||||
tags:
|
tags:
|
||||||
- blocks
|
- blocks
|
||||||
|
/api/v1/bookmarks:
|
||||||
|
get:
|
||||||
|
description: Get an array of statuses bookmarked in the instance
|
||||||
|
operationId: bookmarksGet
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Array of bookmarked statuses
|
||||||
|
headers:
|
||||||
|
Link:
|
||||||
|
description: Links to the next and previous queries.
|
||||||
|
type: string
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/status'
|
||||||
|
type: array
|
||||||
|
"401":
|
||||||
|
description: unauthorized
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
|
security:
|
||||||
|
- OAuth2 Bearer:
|
||||||
|
- read:bookmarks
|
||||||
|
tags:
|
||||||
|
- bookmarks
|
||||||
/api/v1/custom_emojis:
|
/api/v1/custom_emojis:
|
||||||
get:
|
get:
|
||||||
operationId: customEmojisGet
|
operationId: customEmojisGet
|
||||||
|
@ -4111,6 +4139,40 @@ paths:
|
||||||
summary: View status with the given ID.
|
summary: View status with the given ID.
|
||||||
tags:
|
tags:
|
||||||
- statuses
|
- statuses
|
||||||
|
/api/v1/statuses/{id}/bookmark:
|
||||||
|
post:
|
||||||
|
operationId: statusBookmark
|
||||||
|
parameters:
|
||||||
|
- description: Target status ID.
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The status.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/status'
|
||||||
|
"400":
|
||||||
|
description: bad request
|
||||||
|
"401":
|
||||||
|
description: unauthorized
|
||||||
|
"403":
|
||||||
|
description: forbidden
|
||||||
|
"404":
|
||||||
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
|
security:
|
||||||
|
- OAuth2 Bearer:
|
||||||
|
- write:statuses
|
||||||
|
summary: Bookmark status with the given ID.
|
||||||
|
tags:
|
||||||
|
- statuses
|
||||||
/api/v1/statuses/{id}/context:
|
/api/v1/statuses/{id}/context:
|
||||||
get:
|
get:
|
||||||
description: The returned statuses will be ordered in a thread structure, so they are suitable to be displayed in the order in which they were returned.
|
description: The returned statuses will be ordered in a thread structure, so they are suitable to be displayed in the order in which they were returned.
|
||||||
|
@ -4285,6 +4347,40 @@ paths:
|
||||||
summary: View accounts that have reblogged/boosted the target status.
|
summary: View accounts that have reblogged/boosted the target status.
|
||||||
tags:
|
tags:
|
||||||
- statuses
|
- statuses
|
||||||
|
/api/v1/statuses/{id}/unbookmark:
|
||||||
|
post:
|
||||||
|
operationId: statusUnbookmark
|
||||||
|
parameters:
|
||||||
|
- description: Target status ID.
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The status.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/status'
|
||||||
|
"400":
|
||||||
|
description: bad request
|
||||||
|
"401":
|
||||||
|
description: unauthorized
|
||||||
|
"403":
|
||||||
|
description: forbidden
|
||||||
|
"404":
|
||||||
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
|
security:
|
||||||
|
- OAuth2 Bearer:
|
||||||
|
- write:statuses
|
||||||
|
summary: Unbookmark status with the given ID.
|
||||||
|
tags:
|
||||||
|
- statuses
|
||||||
/api/v1/statuses/{id}/unfavourite:
|
/api/v1/statuses/{id}/unfavourite:
|
||||||
post:
|
post:
|
||||||
operationId: statusUnfave
|
operationId: statusUnfave
|
||||||
|
|
50
internal/api/client/bookmarks/bookmarks.go
Normal file
50
internal/api/client/bookmarks/bookmarks.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bookmarks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// BasePath is the base path for serving the bookmarks API
|
||||||
|
BasePath = "/api/v1/bookmarks"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Module implements the ClientAPIModule interface for everything related to bookmarks
|
||||||
|
type Module struct {
|
||||||
|
processor processing.Processor
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new emoji module
|
||||||
|
func New(processor processing.Processor) api.ClientModule {
|
||||||
|
return &Module{
|
||||||
|
processor: processor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route attaches all routes from this module to the given router
|
||||||
|
func (m *Module) Route(r router.Router) error {
|
||||||
|
r.AttachHandler(http.MethodGet, BasePath, m.BookmarksGETHandler)
|
||||||
|
return nil
|
||||||
|
}
|
148
internal/api/client/bookmarks/bookmarks_test.go
Normal file
148
internal/api/client/bookmarks/bookmarks_test.go
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bookmarks_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/bookmarks"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/status"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/concurrency"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/storage"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BookmarkTestSuite struct {
|
||||||
|
// standard suite interfaces
|
||||||
|
suite.Suite
|
||||||
|
db db.DB
|
||||||
|
tc typeutils.TypeConverter
|
||||||
|
mediaManager media.Manager
|
||||||
|
federator federation.Federator
|
||||||
|
emailSender email.Sender
|
||||||
|
processor processing.Processor
|
||||||
|
storage *storage.Driver
|
||||||
|
|
||||||
|
// standard suite models
|
||||||
|
testTokens map[string]*gtsmodel.Token
|
||||||
|
testClients map[string]*gtsmodel.Client
|
||||||
|
testApplications map[string]*gtsmodel.Application
|
||||||
|
testUsers map[string]*gtsmodel.User
|
||||||
|
testAccounts map[string]*gtsmodel.Account
|
||||||
|
testAttachments map[string]*gtsmodel.MediaAttachment
|
||||||
|
testStatuses map[string]*gtsmodel.Status
|
||||||
|
testFollows map[string]*gtsmodel.Follow
|
||||||
|
|
||||||
|
// module being tested
|
||||||
|
statusModule *status.Module
|
||||||
|
bookmarkModule *bookmarks.Module
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BookmarkTestSuite) SetupSuite() {
|
||||||
|
suite.testTokens = testrig.NewTestTokens()
|
||||||
|
suite.testClients = testrig.NewTestClients()
|
||||||
|
suite.testApplications = testrig.NewTestApplications()
|
||||||
|
suite.testUsers = testrig.NewTestUsers()
|
||||||
|
suite.testAccounts = testrig.NewTestAccounts()
|
||||||
|
suite.testAttachments = testrig.NewTestAttachments()
|
||||||
|
suite.testStatuses = testrig.NewTestStatuses()
|
||||||
|
suite.testFollows = testrig.NewTestFollows()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BookmarkTestSuite) SetupTest() {
|
||||||
|
testrig.InitTestConfig()
|
||||||
|
testrig.InitTestLog()
|
||||||
|
|
||||||
|
suite.db = testrig.NewTestDB()
|
||||||
|
suite.tc = testrig.NewTestTypeConverter(suite.db)
|
||||||
|
suite.storage = testrig.NewInMemoryStorage()
|
||||||
|
testrig.StandardDBSetup(suite.db, nil)
|
||||||
|
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
|
||||||
|
|
||||||
|
fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
|
||||||
|
clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1)
|
||||||
|
|
||||||
|
suite.mediaManager = testrig.NewTestMediaManager(suite.db, suite.storage)
|
||||||
|
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker), suite.storage, suite.mediaManager, fedWorker)
|
||||||
|
suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil)
|
||||||
|
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker)
|
||||||
|
suite.statusModule = status.New(suite.processor).(*status.Module)
|
||||||
|
suite.bookmarkModule = bookmarks.New(suite.processor).(*bookmarks.Module)
|
||||||
|
|
||||||
|
suite.NoError(suite.processor.Start())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BookmarkTestSuite) TearDownTest() {
|
||||||
|
testrig.StandardDBTeardown(suite.db)
|
||||||
|
testrig.StandardStorageTeardown(suite.storage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *BookmarkTestSuite) TestGetBookmark() {
|
||||||
|
t := suite.testTokens["local_account_1"]
|
||||||
|
oauthToken := oauth.DBTokenToToken(t)
|
||||||
|
|
||||||
|
targetStatus := suite.testStatuses["admin_account_status_1"]
|
||||||
|
|
||||||
|
// setup
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||||
|
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||||
|
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
||||||
|
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||||
|
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||||
|
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.BookmarkPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting
|
||||||
|
ctx.Request.Header.Set("accept", "application/json")
|
||||||
|
|
||||||
|
suite.bookmarkModule.BookmarksGETHandler(ctx)
|
||||||
|
|
||||||
|
// check response
|
||||||
|
suite.EqualValues(http.StatusOK, recorder.Code)
|
||||||
|
|
||||||
|
result := recorder.Result()
|
||||||
|
defer result.Body.Close()
|
||||||
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
var statuses []model.Status
|
||||||
|
err = json.Unmarshal(b, &statuses)
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
suite.Equal(1, len(statuses))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBookmarkTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(BookmarkTestSuite))
|
||||||
|
}
|
107
internal/api/client/bookmarks/bookmarksget.go
Normal file
107
internal/api/client/bookmarks/bookmarksget.go
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
package bookmarks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// LimitKey is for setting the return amount limit for eg., requesting an account's statuses
|
||||||
|
LimitKey = "limit"
|
||||||
|
|
||||||
|
// MaxIDKey is for specifying the maximum ID of the bookmark to retrieve.
|
||||||
|
MaxIDKey = "max_id"
|
||||||
|
// MinIDKey is for specifying the minimum ID of the bookmark to retrieve.
|
||||||
|
MinIDKey = "min_id"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BookmarksGETHandler swagger:operation GET /api/v1/bookmarks bookmarksGet
|
||||||
|
//
|
||||||
|
// Get an array of statuses bookmarked in the instance
|
||||||
|
//
|
||||||
|
// ---
|
||||||
|
// tags:
|
||||||
|
// - bookmarks
|
||||||
|
//
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
//
|
||||||
|
// security:
|
||||||
|
// - OAuth2 Bearer:
|
||||||
|
// - read:bookmarks
|
||||||
|
//
|
||||||
|
// responses:
|
||||||
|
// '200':
|
||||||
|
// description: Array of bookmarked statuses
|
||||||
|
// schema:
|
||||||
|
// type: array
|
||||||
|
// items:
|
||||||
|
// "$ref": "#/definitions/status"
|
||||||
|
// headers:
|
||||||
|
// Link:
|
||||||
|
// type: string
|
||||||
|
// description: Links to the next and previous queries.
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
|
func (m *Module) BookmarksGETHandler(c *gin.Context) {
|
||||||
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
|
if err != nil {
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
limit := 30
|
||||||
|
limitString := c.Query(LimitKey)
|
||||||
|
if limitString != "" {
|
||||||
|
i, err := strconv.ParseInt(limitString, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
limit = int(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
maxID := ""
|
||||||
|
maxIDString := c.Query(MaxIDKey)
|
||||||
|
if maxIDString != "" {
|
||||||
|
maxID = maxIDString
|
||||||
|
}
|
||||||
|
|
||||||
|
minID := ""
|
||||||
|
minIDString := c.Query(MinIDKey)
|
||||||
|
if minIDString != "" {
|
||||||
|
minID = minIDString
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, errWithCode := m.processor.BookmarksGet(c.Request.Context(), authed, maxID, minID, limit)
|
||||||
|
if errWithCode != nil {
|
||||||
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if errWithCode != nil {
|
||||||
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.LinkHeader != "" {
|
||||||
|
c.Header("Link", resp.LinkHeader)
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, resp.Items)
|
||||||
|
}
|
|
@ -96,6 +96,9 @@ func (m *Module) Route(r router.Router) error {
|
||||||
r.AttachHandler(http.MethodPost, UnreblogPath, m.StatusUnboostPOSTHandler)
|
r.AttachHandler(http.MethodPost, UnreblogPath, m.StatusUnboostPOSTHandler)
|
||||||
r.AttachHandler(http.MethodGet, RebloggedPath, m.StatusBoostedByGETHandler)
|
r.AttachHandler(http.MethodGet, RebloggedPath, m.StatusBoostedByGETHandler)
|
||||||
|
|
||||||
|
r.AttachHandler(http.MethodPost, BookmarkPath, m.StatusBookmarkPOSTHandler)
|
||||||
|
r.AttachHandler(http.MethodPost, UnbookmarkPath, m.StatusUnbookmarkPOSTHandler)
|
||||||
|
|
||||||
r.AttachHandler(http.MethodGet, ContextPath, m.StatusContextGETHandler)
|
r.AttachHandler(http.MethodGet, ContextPath, m.StatusContextGETHandler)
|
||||||
|
|
||||||
r.AttachHandler(http.MethodGet, BasePathWithID, m.muxHandler)
|
r.AttachHandler(http.MethodGet, BasePathWithID, m.muxHandler)
|
||||||
|
|
98
internal/api/client/status/statusbookmark.go
Normal file
98
internal/api/client/status/statusbookmark.go
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StatusBookmarkPOSTHandler swagger:operation POST /api/v1/statuses/{id}/bookmark statusBookmark
|
||||||
|
//
|
||||||
|
// Bookmark status with the given ID.
|
||||||
|
//
|
||||||
|
// ---
|
||||||
|
// tags:
|
||||||
|
// - statuses
|
||||||
|
//
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
//
|
||||||
|
// parameters:
|
||||||
|
// -
|
||||||
|
// name: id
|
||||||
|
// type: string
|
||||||
|
// description: Target status ID.
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
//
|
||||||
|
// security:
|
||||||
|
// - OAuth2 Bearer:
|
||||||
|
// - write:statuses
|
||||||
|
//
|
||||||
|
// responses:
|
||||||
|
// '200':
|
||||||
|
// name: status
|
||||||
|
// description: The status.
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/status"
|
||||||
|
// '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) StatusBookmarkPOSTHandler(c *gin.Context) {
|
||||||
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
|
if err != nil {
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
targetStatusID := c.Param(IDKey)
|
||||||
|
if targetStatusID == "" {
|
||||||
|
err := errors.New("no status id specified")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiStatus, errWithCode := m.processor.StatusBookmark(c.Request.Context(), authed, targetStatusID)
|
||||||
|
if errWithCode != nil {
|
||||||
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, apiStatus)
|
||||||
|
}
|
83
internal/api/client/status/statusbookmark_test.go
Normal file
83
internal/api/client/status/statusbookmark_test.go
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package status_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/status"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StatusBookmarkTestSuite struct {
|
||||||
|
StatusStandardTestSuite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *StatusBookmarkTestSuite) TestPostBookmark() {
|
||||||
|
t := suite.testTokens["local_account_1"]
|
||||||
|
oauthToken := oauth.DBTokenToToken(t)
|
||||||
|
|
||||||
|
targetStatus := suite.testStatuses["admin_account_status_1"]
|
||||||
|
|
||||||
|
// setup
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||||
|
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||||
|
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
||||||
|
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||||
|
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||||
|
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.BookmarkPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting
|
||||||
|
ctx.Request.Header.Set("accept", "application/json")
|
||||||
|
|
||||||
|
// normally the router would populate these params from the path values,
|
||||||
|
// but because we're calling the function directly, we need to set them manually.
|
||||||
|
ctx.Params = gin.Params{
|
||||||
|
gin.Param{
|
||||||
|
Key: status.IDKey,
|
||||||
|
Value: targetStatus.ID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.statusModule.StatusBookmarkPOSTHandler(ctx)
|
||||||
|
|
||||||
|
// check response
|
||||||
|
suite.EqualValues(http.StatusOK, recorder.Code)
|
||||||
|
|
||||||
|
result := recorder.Result()
|
||||||
|
defer result.Body.Close()
|
||||||
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
statusReply := &model.Status{}
|
||||||
|
err = json.Unmarshal(b, statusReply)
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
suite.True(statusReply.Bookmarked)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatusBookmarkTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(StatusBookmarkTestSuite))
|
||||||
|
}
|
98
internal/api/client/status/statusunbookmark.go
Normal file
98
internal/api/client/status/statusunbookmark.go
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StatusUnbookmarkPOSTHandler swagger:operation POST /api/v1/statuses/{id}/unbookmark statusUnbookmark
|
||||||
|
//
|
||||||
|
// Unbookmark status with the given ID.
|
||||||
|
//
|
||||||
|
// ---
|
||||||
|
// tags:
|
||||||
|
// - statuses
|
||||||
|
//
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
//
|
||||||
|
// parameters:
|
||||||
|
// -
|
||||||
|
// name: id
|
||||||
|
// type: string
|
||||||
|
// description: Target status ID.
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
//
|
||||||
|
// security:
|
||||||
|
// - OAuth2 Bearer:
|
||||||
|
// - write:statuses
|
||||||
|
//
|
||||||
|
// responses:
|
||||||
|
// '200':
|
||||||
|
// name: status
|
||||||
|
// description: The status.
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/status"
|
||||||
|
// '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) StatusUnbookmarkPOSTHandler(c *gin.Context) {
|
||||||
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
|
if err != nil {
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
targetStatusID := c.Param(IDKey)
|
||||||
|
if targetStatusID == "" {
|
||||||
|
err := errors.New("no status id specified")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiStatus, errWithCode := m.processor.StatusUnbookmark(c.Request.Context(), authed, targetStatusID)
|
||||||
|
if errWithCode != nil {
|
||||||
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, apiStatus)
|
||||||
|
}
|
78
internal/api/client/status/statusunbookmark_test.go
Normal file
78
internal/api/client/status/statusunbookmark_test.go
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package status_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/status"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StatusUnbookmarkTestSuite struct {
|
||||||
|
StatusStandardTestSuite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *StatusUnbookmarkTestSuite) TestPostUnbookmark() {
|
||||||
|
t := suite.testTokens["local_account_1"]
|
||||||
|
oauthToken := oauth.DBTokenToToken(t)
|
||||||
|
|
||||||
|
targetStatus := suite.testStatuses["admin_account_status_1"]
|
||||||
|
|
||||||
|
// setup
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||||
|
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||||
|
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
||||||
|
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||||
|
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||||
|
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.UnbookmarkPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting
|
||||||
|
ctx.Request.Header.Set("accept", "application/json")
|
||||||
|
|
||||||
|
ctx.Params = gin.Params{
|
||||||
|
gin.Param{
|
||||||
|
Key: status.IDKey,
|
||||||
|
Value: targetStatus.ID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.statusModule.StatusUnbookmarkPOSTHandler(ctx)
|
||||||
|
|
||||||
|
result := recorder.Result()
|
||||||
|
defer result.Body.Close()
|
||||||
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
statusReply := &model.Status{}
|
||||||
|
err = json.Unmarshal(b, statusReply)
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
suite.False(statusReply.Bookmarked)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatusUnbookmarkTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(StatusUnbookmarkTestSuite))
|
||||||
|
}
|
|
@ -73,6 +73,8 @@ type Account interface {
|
||||||
// or replies.
|
// or replies.
|
||||||
GetAccountWebStatuses(ctx context.Context, accountID string, limit int, maxID string) ([]*gtsmodel.Status, Error)
|
GetAccountWebStatuses(ctx context.Context, accountID string, limit int, maxID string) ([]*gtsmodel.Status, Error)
|
||||||
|
|
||||||
|
GetBookmarks(ctx context.Context, accountID string, limit int, maxID string, minID string) ([]*gtsmodel.StatusBookmark, Error)
|
||||||
|
|
||||||
GetAccountBlocks(ctx context.Context, accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, Error)
|
GetAccountBlocks(ctx context.Context, accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, Error)
|
||||||
|
|
||||||
// GetAccountLastPosted simply gets the timestamp of the most recent post by the account.
|
// GetAccountLastPosted simply gets the timestamp of the most recent post by the account.
|
||||||
|
|
|
@ -442,6 +442,38 @@ func (a *accountDB) GetAccountWebStatuses(ctx context.Context, accountID string,
|
||||||
return a.statusesFromIDs(ctx, statusIDs)
|
return a.statusesFromIDs(ctx, statusIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *accountDB) GetBookmarks(ctx context.Context, accountID string, limit int, maxID string, minID string) ([]*gtsmodel.StatusBookmark, db.Error) {
|
||||||
|
bookmarks := []*gtsmodel.StatusBookmark{}
|
||||||
|
|
||||||
|
q := a.conn.
|
||||||
|
NewSelect().
|
||||||
|
TableExpr("? AS ?", bun.Ident("status_bookmarks"), bun.Ident("status_bookmark")).
|
||||||
|
Order("status_bookmark.id DESC").
|
||||||
|
Where("? = ?", bun.Ident("status_bookmark.account_id"), accountID)
|
||||||
|
|
||||||
|
if accountID == "" {
|
||||||
|
return nil, errors.New("must provide an account")
|
||||||
|
}
|
||||||
|
|
||||||
|
if limit != 0 {
|
||||||
|
q = q.Limit(limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxID != "" {
|
||||||
|
q = q.Where("? < ?", bun.Ident("status_bookmark.id"), maxID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if minID != "" {
|
||||||
|
q = q.Where("? > ?", bun.Ident("status_bookmark.id"), minID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := q.Scan(ctx, &bookmarks); err != nil {
|
||||||
|
return nil, a.conn.ProcessError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bookmarks, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *accountDB) GetAccountBlocks(ctx context.Context, accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, db.Error) {
|
func (a *accountDB) GetAccountBlocks(ctx context.Context, accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, db.Error) {
|
||||||
blocks := []*gtsmodel.Block{}
|
blocks := []*gtsmodel.Block{}
|
||||||
|
|
||||||
|
|
|
@ -208,6 +208,12 @@ func (suite *AccountTestSuite) TestInsertAccountWithDefaults() {
|
||||||
suite.False(*newAccount.HideCollections)
|
suite.False(*newAccount.HideCollections)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *AccountTestSuite) TestGettingBookmarksWithNoAccount() {
|
||||||
|
statuses, err := suite.db.GetBookmarks(context.Background(), "", 10, "", "")
|
||||||
|
suite.Error(err)
|
||||||
|
suite.Nil(statuses)
|
||||||
|
}
|
||||||
|
|
||||||
func TestAccountTestSuite(t *testing.T) {
|
func TestAccountTestSuite(t *testing.T) {
|
||||||
suite.Run(t, new(AccountTestSuite))
|
suite.Run(t, new(AccountTestSuite))
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,6 +64,9 @@ type Processor interface {
|
||||||
// WebStatusesGet fetches a number of statuses (in descending order) from the given account. It selects only
|
// WebStatusesGet fetches a number of statuses (in descending order) from the given account. It selects only
|
||||||
// statuses which are suitable for showing on the public web profile of an account.
|
// statuses which are suitable for showing on the public web profile of an account.
|
||||||
WebStatusesGet(ctx context.Context, targetAccountID string, maxID string) (*apimodel.PageableResponse, gtserror.WithCode)
|
WebStatusesGet(ctx context.Context, targetAccountID string, maxID string) (*apimodel.PageableResponse, gtserror.WithCode)
|
||||||
|
// StatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for
|
||||||
|
// the account given in authed.
|
||||||
|
BookmarksGet(ctx context.Context, requestingAccount *gtsmodel.Account, limit int, maxID string, minID string) (*apimodel.PageableResponse, gtserror.WithCode)
|
||||||
// FollowersGet fetches a list of the target account's followers.
|
// FollowersGet fetches a list of the target account's followers.
|
||||||
FollowersGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode)
|
FollowersGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode)
|
||||||
// FollowingGet fetches a list of the accounts that target account is following.
|
// FollowingGet fetches a list of the accounts that target account is following.
|
||||||
|
|
88
internal/processing/account/getbookmarks.go
Normal file
88
internal/processing/account/getbookmarks.go
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package account
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *processor) BookmarksGet(ctx context.Context, requestingAccount *gtsmodel.Account, limit int, maxID string, minID string) (*apimodel.PageableResponse, gtserror.WithCode) {
|
||||||
|
if requestingAccount == nil {
|
||||||
|
return nil, gtserror.NewErrorForbidden(fmt.Errorf("cannot retrieve bookmarks without a requesting account"))
|
||||||
|
}
|
||||||
|
|
||||||
|
bookmarks, err := p.db.GetBookmarks(ctx, requestingAccount.ID, limit, maxID, minID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
count := len(bookmarks)
|
||||||
|
filtered := make([]*gtsmodel.Status, 0, len(bookmarks))
|
||||||
|
nextMaxIDValue := ""
|
||||||
|
prevMinIDValue := ""
|
||||||
|
for i, b := range bookmarks {
|
||||||
|
s, err := p.db.GetStatusByID(ctx, b.StatusID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
visible, err := p.filter.StatusVisible(ctx, s, requestingAccount)
|
||||||
|
if err == nil && visible {
|
||||||
|
if i == count-1 {
|
||||||
|
nextMaxIDValue = b.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
prevMinIDValue = b.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered = append(filtered, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
count = len(filtered)
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
return util.EmptyPageableResponse(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
items := []interface{}{}
|
||||||
|
for _, s := range filtered {
|
||||||
|
item, err := p.tc.StatusToAPIStatus(ctx, s, requestingAccount)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status to api: %s", err))
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.PackagePageableResponse(util.PageableResponseParams{
|
||||||
|
Items: items,
|
||||||
|
Path: "/api/v1/bookmarks",
|
||||||
|
NextMaxIDValue: nextMaxIDValue,
|
||||||
|
PrevMinIDValue: prevMinIDValue,
|
||||||
|
Limit: limit,
|
||||||
|
ExtraQueryParams: []string{},
|
||||||
|
})
|
||||||
|
}
|
31
internal/processing/bookmark.go
Normal file
31
internal/processing/bookmark.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package processing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *processor) BookmarksGet(ctx context.Context, authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) {
|
||||||
|
return p.accountProcessor.BookmarksGet(ctx, authed.Account, limit, maxID, minID)
|
||||||
|
}
|
|
@ -146,6 +146,9 @@ type Processor interface {
|
||||||
// CustomEmojisGet returns an array of info about the custom emojis on this server
|
// CustomEmojisGet returns an array of info about the custom emojis on this server
|
||||||
CustomEmojisGet(ctx context.Context) ([]*apimodel.Emoji, gtserror.WithCode)
|
CustomEmojisGet(ctx context.Context) ([]*apimodel.Emoji, gtserror.WithCode)
|
||||||
|
|
||||||
|
// BookmarksGet returns a pageable response of statuses that have been bookmarked
|
||||||
|
BookmarksGet(ctx context.Context, authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode)
|
||||||
|
|
||||||
// FileGet handles the fetching of a media attachment file via the fileserver.
|
// FileGet handles the fetching of a media attachment file via the fileserver.
|
||||||
FileGet(ctx context.Context, authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, gtserror.WithCode)
|
FileGet(ctx context.Context, authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, gtserror.WithCode)
|
||||||
|
|
||||||
|
@ -202,6 +205,10 @@ type Processor interface {
|
||||||
StatusUnfave(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
|
StatusUnfave(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
|
||||||
// StatusGetContext returns the context (previous and following posts) from the given status ID
|
// StatusGetContext returns the context (previous and following posts) from the given status ID
|
||||||
StatusGetContext(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Context, gtserror.WithCode)
|
StatusGetContext(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Context, gtserror.WithCode)
|
||||||
|
// StatusBookmark process a bookmark for a status
|
||||||
|
StatusBookmark(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
|
||||||
|
// StatusUnbookmark removes a bookmark for a status
|
||||||
|
StatusUnbookmark(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
|
||||||
|
|
||||||
// HomeTimelineGet returns statuses from the home timeline, with the given filters/parameters.
|
// HomeTimelineGet returns statuses from the home timeline, with the given filters/parameters.
|
||||||
HomeTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.PageableResponse, gtserror.WithCode)
|
HomeTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.PageableResponse, gtserror.WithCode)
|
||||||
|
|
|
@ -65,3 +65,11 @@ func (p *processor) StatusUnfave(ctx context.Context, authed *oauth.Auth, target
|
||||||
func (p *processor) StatusGetContext(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Context, gtserror.WithCode) {
|
func (p *processor) StatusGetContext(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Context, gtserror.WithCode) {
|
||||||
return p.statusProcessor.Context(ctx, authed.Account, targetStatusID)
|
return p.statusProcessor.Context(ctx, authed.Account, targetStatusID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *processor) StatusBookmark(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
||||||
|
return p.statusProcessor.Bookmark(ctx, authed.Account, targetStatusID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *processor) StatusUnbookmark(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
||||||
|
return p.statusProcessor.Unbookmark(ctx, authed.Account, targetStatusID)
|
||||||
|
}
|
||||||
|
|
86
internal/processing/status/bookmark.go
Normal file
86
internal/processing/status/bookmark.go
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *processor) Bookmark(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
||||||
|
targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
|
||||||
|
}
|
||||||
|
if targetStatus.Account == nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
|
||||||
|
}
|
||||||
|
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
|
||||||
|
}
|
||||||
|
if !visible {
|
||||||
|
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// first check if the status is already bookmarked, if so we don't need to do anything
|
||||||
|
newBookmark := true
|
||||||
|
gtsBookmark := >smodel.StatusBookmark{}
|
||||||
|
if err := p.db.GetWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsBookmark); err == nil {
|
||||||
|
// we already have a bookmark for this status
|
||||||
|
newBookmark = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if newBookmark {
|
||||||
|
thisBookmarkID, err := id.NewULID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// we need to create a new bookmark in the database
|
||||||
|
gtsBookmark := >smodel.StatusBookmark{
|
||||||
|
ID: thisBookmarkID,
|
||||||
|
AccountID: requestingAccount.ID,
|
||||||
|
Account: requestingAccount,
|
||||||
|
TargetAccountID: targetStatus.AccountID,
|
||||||
|
TargetAccount: targetStatus.Account,
|
||||||
|
StatusID: targetStatus.ID,
|
||||||
|
Status: targetStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.db.Put(ctx, gtsBookmark); err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error putting bookmark in database: %s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the apidon representation of the target status
|
||||||
|
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiStatus, nil
|
||||||
|
}
|
48
internal/processing/status/bookmark_test.go
Normal file
48
internal/processing/status/bookmark_test.go
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package status_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StatusBookmarkTestSuite struct {
|
||||||
|
StatusStandardTestSuite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *StatusBookmarkTestSuite) TestBookmark() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// bookmark a status
|
||||||
|
bookmarkingAccount1 := suite.testAccounts["local_account_1"]
|
||||||
|
targetStatus1 := suite.testStatuses["admin_account_status_1"]
|
||||||
|
|
||||||
|
bookmark1, err := suite.status.Bookmark(ctx, bookmarkingAccount1, targetStatus1.ID)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(bookmark1)
|
||||||
|
suite.True(bookmark1.Bookmarked)
|
||||||
|
suite.Equal(targetStatus1.ID, bookmark1.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatusBookmarkTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(StatusBookmarkTestSuite))
|
||||||
|
}
|
|
@ -54,6 +54,10 @@ type Processor interface {
|
||||||
Unfave(ctx context.Context, account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
|
Unfave(ctx context.Context, account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
|
||||||
// Context returns the context (previous and following posts) from the given status ID
|
// Context returns the context (previous and following posts) from the given status ID
|
||||||
Context(ctx context.Context, account *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode)
|
Context(ctx context.Context, account *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode)
|
||||||
|
// Bookmarks a status
|
||||||
|
Bookmark(ctx context.Context, account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
|
||||||
|
// Removes a bookmark for a status
|
||||||
|
Unbookmark(ctx context.Context, account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
PROCESSING UTILS
|
PROCESSING UTILS
|
||||||
|
|
69
internal/processing/status/unbookmark.go
Normal file
69
internal/processing/status/unbookmark.go
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *processor) Unbookmark(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
||||||
|
targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
|
||||||
|
}
|
||||||
|
if targetStatus.Account == nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
|
||||||
|
}
|
||||||
|
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
|
||||||
|
}
|
||||||
|
if !visible {
|
||||||
|
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// first check if the status is already bookmarked
|
||||||
|
toUnbookmark := false
|
||||||
|
gtsBookmark := >smodel.StatusBookmark{}
|
||||||
|
if err := p.db.GetWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsBookmark); err == nil {
|
||||||
|
// we already have a bookmark for this status
|
||||||
|
toUnbookmark = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if toUnbookmark {
|
||||||
|
if err := p.db.DeleteWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsBookmark); err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unfaveing status: %s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the apidon representation of the target status
|
||||||
|
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiStatus, nil
|
||||||
|
}
|
54
internal/processing/status/unbookmark_test.go
Normal file
54
internal/processing/status/unbookmark_test.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package status_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StatusUnbookmarkTestSuite struct {
|
||||||
|
StatusStandardTestSuite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *StatusUnbookmarkTestSuite) TestUnbookmark() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// bookmark a status
|
||||||
|
bookmarkingAccount1 := suite.testAccounts["local_account_1"]
|
||||||
|
targetStatus1 := suite.testStatuses["admin_account_status_1"]
|
||||||
|
|
||||||
|
bookmark1, err := suite.status.Bookmark(ctx, bookmarkingAccount1, targetStatus1.ID)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(bookmark1)
|
||||||
|
suite.True(bookmark1.Bookmarked)
|
||||||
|
suite.Equal(targetStatus1.ID, bookmark1.ID)
|
||||||
|
|
||||||
|
bookmark2, err := suite.status.Unbookmark(ctx, bookmarkingAccount1, targetStatus1.ID)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(bookmark2)
|
||||||
|
suite.False(bookmark2.Bookmarked)
|
||||||
|
suite.Equal(targetStatus1.ID, bookmark1.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatusUnbookmarkTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(StatusUnbookmarkTestSuite))
|
||||||
|
}
|
|
@ -93,7 +93,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontend() {
|
||||||
b, err := json.Marshal(apiStatus)
|
b, err := json.Marshal(apiStatus)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
suite.Equal(`{"id":"01F8MH75CBF9JFX4ZAD54N0W0R","created_at":"2021-10-20T11:36:45.000Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"public","language":"en","uri":"http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R","url":"http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R","replies_count":0,"reblogs_count":0,"favourites_count":1,"favourited":true,"reblogged":false,"muted":false,"bookmarked":false,"pinned":false,"content":"hello world! #welcome ! first post on the instance :rainbow: !","reblog":null,"application":{"name":"superseriousbusiness","website":"https://superserious.business"},"account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[],"enable_rss":true,"role":"admin"},"media_attachments":[{"id":"01F8MH6NEM8D7527KZAECTCR76","type":"image","url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpeg","text_url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpeg","preview_url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/small/01F8MH6NEM8D7527KZAECTCR76.jpeg","remote_url":null,"preview_remote_url":null,"meta":{"original":{"width":1200,"height":630,"size":"1200x630","aspect":1.9047619},"small":{"width":256,"height":134,"size":"256x134","aspect":1.9104477},"focus":{"x":0,"y":0}},"description":"Black and white image of some 50's style text saying: Welcome On Board","blurhash":"LNJRdVM{00Rj%Mayt7j[4nWBofRj"}],"mentions":[],"tags":[{"name":"welcome","url":"http://localhost:8080/tags/welcome"}],"emojis":[{"shortcode":"rainbow","url":"http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png","static_url":"http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/static/01F8MH9H8E4VG3KDYJR9EGPXCQ.png","visible_in_picker":true,"category":"reactions"}],"card":null,"poll":null,"text":"hello world! #welcome ! first post on the instance :rainbow: !"}`, string(b))
|
suite.Equal(`{"id":"01F8MH75CBF9JFX4ZAD54N0W0R","created_at":"2021-10-20T11:36:45.000Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"public","language":"en","uri":"http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R","url":"http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R","replies_count":0,"reblogs_count":0,"favourites_count":1,"favourited":true,"reblogged":false,"muted":false,"bookmarked":true,"pinned":false,"content":"hello world! #welcome ! first post on the instance :rainbow: !","reblog":null,"application":{"name":"superseriousbusiness","website":"https://superserious.business"},"account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[],"enable_rss":true,"role":"admin"},"media_attachments":[{"id":"01F8MH6NEM8D7527KZAECTCR76","type":"image","url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpeg","text_url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpeg","preview_url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/small/01F8MH6NEM8D7527KZAECTCR76.jpeg","remote_url":null,"preview_remote_url":null,"meta":{"original":{"width":1200,"height":630,"size":"1200x630","aspect":1.9047619},"small":{"width":256,"height":134,"size":"256x134","aspect":1.9104477},"focus":{"x":0,"y":0}},"description":"Black and white image of some 50's style text saying: Welcome On Board","blurhash":"LNJRdVM{00Rj%Mayt7j[4nWBofRj"}],"mentions":[],"tags":[{"name":"welcome","url":"http://localhost:8080/tags/welcome"}],"emojis":[{"shortcode":"rainbow","url":"http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png","static_url":"http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/static/01F8MH9H8E4VG3KDYJR9EGPXCQ.png","visible_in_picker":true,"category":"reactions"}],"card":null,"poll":null,"text":"hello world! #welcome ! first post on the instance :rainbow: !"}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *InternalToFrontendTestSuite) TestInstanceToFrontend() {
|
func (suite *InternalToFrontendTestSuite) TestInstanceToFrontend() {
|
||||||
|
|
|
@ -261,6 +261,12 @@ func StandardDBSetup(db db.DB, accounts map[string]*gtsmodel.Account) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, v := range NewTestBookmarks() {
|
||||||
|
if err := db.Put(ctx, v); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := db.CreateInstanceAccount(ctx); err != nil {
|
if err := db.CreateInstanceAccount(ctx); err != nil {
|
||||||
log.Panic(err)
|
log.Panic(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2251,6 +2251,26 @@ func NewTestFediStatuses() map[string]vocab.ActivityStreamsNote {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewTestBookmarks returns a map of gts model bookmarks, keyed in the format [bookmarking_account]_[target_status]
|
||||||
|
func NewTestBookmarks() map[string]*gtsmodel.StatusBookmark {
|
||||||
|
return map[string]*gtsmodel.StatusBookmark{
|
||||||
|
"local_account_1_admin_account_status_1": {
|
||||||
|
ID: "01F8MHD2QCZSZ6WQS2ATVPEYJ9",
|
||||||
|
CreatedAt: TimeMustParse("2022-05-14T13:21:09+02:00"),
|
||||||
|
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF", // local account 1
|
||||||
|
TargetAccountID: "01F8MH17FWEB39HZJ76B6VXSKF", // admin account
|
||||||
|
StatusID: "01F8MH75CBF9JFX4ZAD54N0W0R", // admin account status 1
|
||||||
|
},
|
||||||
|
"admin_account_local_account_1_status_1": {
|
||||||
|
ID: "01F8Q0486ANTDWKG02A7DS1Q24",
|
||||||
|
CreatedAt: TimeMustParse("2022-05-14T13:21:09+02:00"),
|
||||||
|
AccountID: "01F8MH17FWEB39HZJ76B6VXSKF", // admin account
|
||||||
|
TargetAccountID: "01F8MH1H7YV1Z7D2C8K2730QBF", // local account 1
|
||||||
|
StatusID: "01F8MHAMCHF6Y650WCRSCP4WMY", // local account status 1
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NewTestDereferenceRequests returns a map of incoming dereference requests, with their signatures.
|
// NewTestDereferenceRequests returns a map of incoming dereference requests, with their signatures.
|
||||||
func NewTestDereferenceRequests(accounts map[string]*gtsmodel.Account) map[string]ActivityWithSignature {
|
func NewTestDereferenceRequests(accounts map[string]*gtsmodel.Account) map[string]ActivityWithSignature {
|
||||||
var sig, digest, date string
|
var sig, digest, date string
|
||||||
|
|
Loading…
Reference in a new issue