mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2025-06-05 21:59:39 +02: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:
@@ -64,6 +64,9 @@ type Processor interface {
|
||||
// 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.
|
||||
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(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode)
|
||||
// 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(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(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)
|
||||
// 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)
|
||||
// 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(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) {
|
||||
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)
|
||||
// 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)
|
||||
// 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
|
||||
|
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))
|
||||
}
|
Reference in New Issue
Block a user