mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-01 06:50:00 +00:00
Deletes+unboosts (#52)
* Status deletes properly streamed now. * Unboosts now work locally and federated. * Documentation updates.
This commit is contained in:
parent
efbd839181
commit
a5fd6f427b
15 changed files with 321 additions and 39 deletions
|
@ -78,7 +78,7 @@
|
||||||
* [x] /api/v1/statuses/:id/favourite POST (Fave a status)
|
* [x] /api/v1/statuses/:id/favourite POST (Fave a status)
|
||||||
* [x] /api/v1/statuses/:id/unfavourite POST (Unfave a status)
|
* [x] /api/v1/statuses/:id/unfavourite POST (Unfave a status)
|
||||||
* [x] /api/v1/statuses/:id/reblog POST (Reblog a status)
|
* [x] /api/v1/statuses/:id/reblog POST (Reblog a status)
|
||||||
* [ ] /api/v1/statuses/:id/unreblog POST (Undo a reblog)
|
* [x] /api/v1/statuses/:id/unreblog POST (Undo a reblog)
|
||||||
* [ ] /api/v1/statuses/:id/bookmark POST (Bookmark a status)
|
* [ ] /api/v1/statuses/:id/bookmark POST (Bookmark a status)
|
||||||
* [ ] /api/v1/statuses/:id/unbookmark POST (Undo a bookmark)
|
* [ ] /api/v1/statuses/:id/unbookmark POST (Undo a bookmark)
|
||||||
* [ ] /api/v1/statuses/:id/mute POST (Mute notifications on a status)
|
* [ ] /api/v1/statuses/:id/mute POST (Mute notifications on a status)
|
||||||
|
@ -118,8 +118,8 @@
|
||||||
* [ ] Markers
|
* [ ] Markers
|
||||||
* [ ] /api/v1/markers GET (Get saved timeline position)
|
* [ ] /api/v1/markers GET (Get saved timeline position)
|
||||||
* [ ] /api/v1/markers POST (Save timeline position)
|
* [ ] /api/v1/markers POST (Save timeline position)
|
||||||
* [ ] Streaming
|
* [x] Streaming
|
||||||
* [ ] /api/v1/streaming WEBSOCKETS (Stream live events to user via websockets)
|
* [x] /api/v1/streaming WEBSOCKETS (Stream live events to user via websockets)
|
||||||
* [ ] Notifications
|
* [ ] Notifications
|
||||||
* [x] /api/v1/notifications GET (Get list of notifications)
|
* [x] /api/v1/notifications GET (Get list of notifications)
|
||||||
* [x] /api/v1/notifications/:id GET (Get a single notification)
|
* [x] /api/v1/notifications/:id GET (Get a single notification)
|
||||||
|
|
58
README.md
58
README.md
|
@ -8,43 +8,57 @@ Federated social media software.
|
||||||
|
|
||||||
GoToSocial is a Fediverse server project, written in Golang. It provides an alternative to existing projects such as [Mastodon](https://joinmastodon.org/), [Pleroma](https://pleroma.social/), [Friendica](https://friendica.net), [PixelFed](https://pixelfed.org/) etc.
|
GoToSocial is a Fediverse server project, written in Golang. It provides an alternative to existing projects such as [Mastodon](https://joinmastodon.org/), [Pleroma](https://pleroma.social/), [Friendica](https://friendica.net), [PixelFed](https://pixelfed.org/) etc.
|
||||||
|
|
||||||
One of the key differences between GoToSocial and those other projects is that GoToSocial doesn't include an integrated front-end (ie., a webapp). Instead, like the Matrix.org's [Synapse](https://github.com/matrix-org/synapse) project, it provides only a server implementation and a well-documented API. On this API, developers are free to build any front-end implementation or mobile application that they wish.
|
One of the key differences between GoToSocial and those other projects is that GoToSocial doesn't include an integrated client front-end (ie., a webapp). Instead, like the Matrix.org's [Synapse](https://github.com/matrix-org/synapse) project, it provides only a server implementation, some static web pages for profiles and posts, and a well-documented API. On this API, developers are free to build any front-end implementation or mobile application that they wish.
|
||||||
|
|
||||||
Because the server implementation is as generic and flexible/configurable as possible, GoToSocial provides the basis for many different types of social media experience, whether Tumblr-like, Facebook-like, or Twitter-like.
|
Because the server implementation is as generic and flexible/configurable as possible, GoToSocial provides the basis for many different types of social media experience, whether Tumblr-like, Facebook-like, or Twitter-like.
|
||||||
|
|
||||||
## Goals
|
## Features Wishlist
|
||||||
|
|
||||||
The first goal of the project is to implement a feature set comparable to Mastodon: server logic, federation logic, and a client API that's a superset of the Mastodon API described [here](https://docs.joinmastodon.org/).
|
A grab-bag of things that are already included or will be included in the project if time allows:
|
||||||
|
|
||||||
Once the client API is implemented, it should allow existing Mastodon apps like [Tusky](https://tusky.app/) and [Whalebird](https://whalebird.social/en/desktop/contents) to work with GoToSocial.
|
* Various federation modes, including reputation-based 'slow' federation, 'normal' federation, allowlist-only federation, and zero federation.
|
||||||
|
* Local-only posting, and granular post settings including 'rebloggable/boostable', 'likeable', 'replyable'.
|
||||||
After that, custom features will be added that will necessitate expanding the API.
|
* Character limit for posts that's easy for admins to configure (no messing around in the source code).
|
||||||
|
* Groups and group posting!
|
||||||
## Wishlist
|
* Built-in, automatic LetsEncrypt support (no messing around with Nginx or Certbot).
|
||||||
|
* Good performance on lower-powered machines like Raspberry Pi, old laptops, tiny VPSes (the test VPS has 1gb of ram and 1 cpu core).
|
||||||
Among other things:
|
* Subscribeable and shareable allowlists/denylists for federation.
|
||||||
|
|
||||||
* Reputation-based 'slow' federation.
|
|
||||||
* Granular post settings.
|
|
||||||
* Local-only posting.
|
|
||||||
* Easily-configurable character limit.
|
|
||||||
* Groups and group posting.
|
|
||||||
|
|
||||||
## Implementation Status
|
## Implementation Status
|
||||||
|
|
||||||
For an up-to-date view on progress made towards a v1.0.0 release, see [here](./PROGRESS.md).
|
Things are moving on the project! As of June 2021 you can now:
|
||||||
|
|
||||||
|
* Build and deploy GoToSocial as a binary, with automatic LetsEncrypt certificate support built-in.
|
||||||
|
* Connect to the running instance via Tusky or Pinafore, using email address and password (stored encrypted).
|
||||||
|
* Post/delete posts.
|
||||||
|
* Reply/delete replies.
|
||||||
|
* Fave/unfave posts.
|
||||||
|
* Post images and gifs.
|
||||||
|
* Boost stuff/unboost stuff.
|
||||||
|
* Set your profile info (including header and avatar).
|
||||||
|
* Follow people/unfollow people.
|
||||||
|
* Accept follow requests from people.
|
||||||
|
* Post followers only/direct/public/unlocked.
|
||||||
|
* Customize posts with further flags: federated (y/n), replyable (y/n), likeable (y/n), boostable (y/n) -- not supported through Pinafore/Tusky yet.
|
||||||
|
* Get notifications for mentions/replies/likes/boosts.
|
||||||
|
* View local timeline.
|
||||||
|
* View and scroll home timeline (with ~10ms latency hell yeah).
|
||||||
|
* Stream new posts, notifications and deletes through a websockets connection via Pinafore.
|
||||||
|
* Federation support and interoperability with Mastodon and others.
|
||||||
|
|
||||||
|
In other words, a deployed GoToSocial instance is already pretty useable!
|
||||||
|
|
||||||
|
For a detailed view on progress made towards a v0.1.0 (beta) release, see [here](./PROGRESS.md).
|
||||||
|
|
||||||
## Contact
|
## Contact
|
||||||
|
|
||||||
For questions and comments, you can reach out to Tobi on the Fediverse [here](https://ondergrond.org/@dumpsterqueer) or mail admin@gotosocial.org.
|
For questions and comments, you can reach out to tobi on the Fediverse [here](https://ondergrond.org/@dumpsterqueer) or mail admin@gotosocial.org.
|
||||||
|
|
||||||
## Sponsorship
|
## Sponsorship
|
||||||
|
|
||||||
Currently, this project is funded using Liberapay, to put bread on the table while Tobi works on it. If you want to sponsor this project and get your name on this repo, you can do so [here](https://liberapay.com/dumpsterqueer/)! `<3`
|
Currently, this project is funded using Liberapay, to put bread on the table while work continues on it.
|
||||||
|
|
||||||
### Sponsors
|
If you want to sponsor this project, you can do so [here](https://liberapay.com/dumpsterqueer/)! `<3`
|
||||||
|
|
||||||
None yet! [Go For It](https://liberapay.com/dumpsterqueer/)
|
|
||||||
|
|
||||||
### Image Attribution
|
### Image Attribution
|
||||||
|
|
||||||
|
|
|
@ -98,6 +98,7 @@ func (m *Module) Route(r router.Router) error {
|
||||||
r.AttachHandler(http.MethodGet, FavouritedPath, m.StatusFavedByGETHandler)
|
r.AttachHandler(http.MethodGet, FavouritedPath, m.StatusFavedByGETHandler)
|
||||||
|
|
||||||
r.AttachHandler(http.MethodPost, ReblogPath, m.StatusBoostPOSTHandler)
|
r.AttachHandler(http.MethodPost, ReblogPath, m.StatusBoostPOSTHandler)
|
||||||
|
r.AttachHandler(http.MethodPost, UnreblogPath, m.StatusUnboostPOSTHandler)
|
||||||
r.AttachHandler(http.MethodGet, RebloggedPath, m.StatusBoostedByGETHandler)
|
r.AttachHandler(http.MethodGet, RebloggedPath, m.StatusBoostedByGETHandler)
|
||||||
|
|
||||||
r.AttachHandler(http.MethodGet, ContextPath, m.StatusContextGETHandler)
|
r.AttachHandler(http.MethodGet, ContextPath, m.StatusContextGETHandler)
|
||||||
|
|
60
internal/api/client/status/statusunboost.go
Normal file
60
internal/api/client/status/statusunboost.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021 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 (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StatusUnboostPOSTHandler handles unboost requests against a given status ID
|
||||||
|
func (m *Module) StatusUnboostPOSTHandler(c *gin.Context) {
|
||||||
|
l := m.log.WithFields(logrus.Fields{
|
||||||
|
"func": "StatusUnboostPOSTHandler",
|
||||||
|
"request_uri": c.Request.RequestURI,
|
||||||
|
"user_agent": c.Request.UserAgent(),
|
||||||
|
"origin_ip": c.ClientIP(),
|
||||||
|
})
|
||||||
|
l.Debugf("entering function")
|
||||||
|
|
||||||
|
authed, err := oauth.Authed(c, true, false, true, true) // we don't really need an app here but we want everything else
|
||||||
|
if err != nil {
|
||||||
|
l.Debug("not authed so can't unboost status")
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
targetStatusID := c.Param(IDKey)
|
||||||
|
if targetStatusID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mastoStatus, errWithCode := m.processor.StatusUnboost(authed, targetStatusID)
|
||||||
|
if errWithCode != nil {
|
||||||
|
l.Debugf("error processing status unboost: %s", errWithCode.Error())
|
||||||
|
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, mastoStatus)
|
||||||
|
}
|
|
@ -138,6 +138,18 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error
|
||||||
return errors.New("undo was not parseable as *gtsmodel.StatusFave")
|
return errors.New("undo was not parseable as *gtsmodel.StatusFave")
|
||||||
}
|
}
|
||||||
return p.federateUnfave(fave, clientMsg.OriginAccount, clientMsg.TargetAccount)
|
return p.federateUnfave(fave, clientMsg.OriginAccount, clientMsg.TargetAccount)
|
||||||
|
case gtsmodel.ActivityStreamsAnnounce:
|
||||||
|
// UNDO ANNOUNCE/BOOST
|
||||||
|
boost, ok := clientMsg.GTSModel.(*gtsmodel.Status)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("undo was not parseable as *gtsmodel.Status")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.deleteStatusFromTimelines(boost); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.federateUnannounce(boost, clientMsg.OriginAccount, clientMsg.TargetAccount)
|
||||||
}
|
}
|
||||||
case gtsmodel.ActivityStreamsDelete:
|
case gtsmodel.ActivityStreamsDelete:
|
||||||
// DELETE
|
// DELETE
|
||||||
|
@ -313,6 +325,36 @@ func (p *processor) federateUnfave(fave *gtsmodel.StatusFave, originAccount *gts
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *processor) federateUnannounce(boost *gtsmodel.Status, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
|
||||||
|
asAnnounce, err := p.tc.BoostToAS(boost, originAccount, targetAccount)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("federateUnannounce: error converting status to announce: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create an Undo and set the appropriate actor on it
|
||||||
|
undo := streams.NewActivityStreamsUndo()
|
||||||
|
undo.SetActivityStreamsActor(asAnnounce.GetActivityStreamsActor())
|
||||||
|
|
||||||
|
// Set the boost as the 'object' property.
|
||||||
|
undoObject := streams.NewActivityStreamsObjectProperty()
|
||||||
|
undoObject.AppendActivityStreamsAnnounce(asAnnounce)
|
||||||
|
undo.SetActivityStreamsObject(undoObject)
|
||||||
|
|
||||||
|
// set the to
|
||||||
|
undo.SetActivityStreamsTo(asAnnounce.GetActivityStreamsTo())
|
||||||
|
|
||||||
|
// set the cc
|
||||||
|
undo.SetActivityStreamsCc(asAnnounce.GetActivityStreamsCc())
|
||||||
|
|
||||||
|
outboxIRI, err := url.Parse(originAccount.OutboxURI)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("federateUnannounce: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, undo)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (p *processor) federateAcceptFollowRequest(follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
|
func (p *processor) federateAcceptFollowRequest(follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
|
||||||
// if both accounts are local there's nothing to do here
|
// if both accounts are local there's nothing to do here
|
||||||
if originAccount.Domain == "" && targetAccount.Domain == "" {
|
if originAccount.Domain == "" && targetAccount.Domain == "" {
|
||||||
|
|
|
@ -401,5 +401,9 @@ func (p *processor) timelineStatusForAccount(status *gtsmodel.Status, accountID
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) deleteStatusFromTimelines(status *gtsmodel.Status) error {
|
func (p *processor) deleteStatusFromTimelines(status *gtsmodel.Status) error {
|
||||||
return p.timelineManager.WipeStatusFromAllTimelines(status.ID)
|
if err := p.timelineManager.WipeStatusFromAllTimelines(status.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.streamingProcessor.StreamDelete(status.ID)
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,6 +117,8 @@ type Processor interface {
|
||||||
StatusFave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error)
|
StatusFave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error)
|
||||||
// StatusBoost processes the boost/reblog of a given status, returning the newly-created boost if all is well.
|
// StatusBoost processes the boost/reblog of a given status, returning the newly-created boost if all is well.
|
||||||
StatusBoost(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
|
StatusBoost(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
|
||||||
|
// StatusUnboost processes the unboost/unreblog of a given status, returning the status if all is well.
|
||||||
|
StatusUnboost(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
|
||||||
// StatusBoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings.
|
// StatusBoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings.
|
||||||
StatusBoostedBy(authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode)
|
StatusBoostedBy(authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode)
|
||||||
// StatusFavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings.
|
// StatusFavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings.
|
||||||
|
|
|
@ -40,6 +40,10 @@ func (p *processor) StatusBoost(authed *oauth.Auth, targetStatusID string) (*api
|
||||||
return p.statusProcessor.Boost(authed.Account, authed.Application, targetStatusID)
|
return p.statusProcessor.Boost(authed.Account, authed.Application, targetStatusID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *processor) StatusUnboost(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
||||||
|
return p.statusProcessor.Unboost(authed.Account, authed.Application, targetStatusID)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *processor) StatusBoostedBy(authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) {
|
func (p *processor) StatusBoostedBy(authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) {
|
||||||
return p.statusProcessor.BoostedBy(authed.Account, targetStatusID)
|
return p.statusProcessor.BoostedBy(authed.Account, targetStatusID)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,8 @@ type Processor interface {
|
||||||
Fave(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
|
Fave(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
|
||||||
// Boost processes the boost/reblog of a given status, returning the newly-created boost if all is well.
|
// Boost processes the boost/reblog of a given status, returning the newly-created boost if all is well.
|
||||||
Boost(account *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
|
Boost(account *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
|
||||||
|
// Unboost processes the unboost/unreblog of a given status, returning the status if all is well.
|
||||||
|
Unboost(account *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
|
||||||
// BoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings.
|
// BoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings.
|
||||||
BoostedBy(account *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode)
|
BoostedBy(account *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode)
|
||||||
// FavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings.
|
// FavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings.
|
||||||
|
|
95
internal/processing/synchronous/status/unboost.go
Normal file
95
internal/processing/synchronous/status/unboost.go
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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) Unboost(account *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
||||||
|
l := p.log.WithField("func", "Unboost")
|
||||||
|
|
||||||
|
l.Tracef("going to search for target status %s", targetStatusID)
|
||||||
|
targetStatus := >smodel.Status{}
|
||||||
|
if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Tracef("going to search for target account %s", targetStatus.AccountID)
|
||||||
|
targetAccount := >smodel.Account{}
|
||||||
|
if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Trace("going to see if status is visible")
|
||||||
|
visible, err := p.filter.StatusVisible(targetStatus, account)
|
||||||
|
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"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we actually have a boost for this status
|
||||||
|
var toUnboost bool
|
||||||
|
|
||||||
|
gtsBoost := >smodel.Status{}
|
||||||
|
where := []db.Where{
|
||||||
|
{
|
||||||
|
Key: "boost_of_id",
|
||||||
|
Value: targetStatusID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "account_id",
|
||||||
|
Value: account.ID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = p.db.GetWhere(where, gtsBoost)
|
||||||
|
if err == nil {
|
||||||
|
// we have a boost
|
||||||
|
toUnboost = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// something went wrong in the db finding the boost
|
||||||
|
if _, ok := err.(db.ErrNoEntries); !ok {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching existing boost from database: %s", err))
|
||||||
|
}
|
||||||
|
// we just don't have a boost
|
||||||
|
toUnboost = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if toUnboost {
|
||||||
|
// we had a boost, so take some action to get rid of it
|
||||||
|
if err := p.db.DeleteWhere(where, >smodel.Status{}); err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unboosting status: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// pin some stuff onto the boost while we have it out of the db
|
||||||
|
gtsBoost.GTSBoostedStatus = targetStatus
|
||||||
|
gtsBoost.GTSBoostedStatus.GTSAuthorAccount = targetAccount
|
||||||
|
gtsBoost.GTSBoostedAccount = targetAccount
|
||||||
|
gtsBoost.GTSAuthorAccount = account
|
||||||
|
|
||||||
|
// send it back to the processor for async processing
|
||||||
|
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
||||||
|
APObjectType: gtsmodel.ActivityStreamsAnnounce,
|
||||||
|
APActivityType: gtsmodel.ActivityStreamsUndo,
|
||||||
|
GTSModel: gtsBoost,
|
||||||
|
OriginAccount: account,
|
||||||
|
TargetAccount: targetAccount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mastoStatus, err := p.tc.StatusToMasto(targetStatus, account)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return mastoStatus, nil
|
||||||
|
}
|
51
internal/processing/synchronous/streaming/streamdelete.go
Normal file
51
internal/processing/synchronous/streaming/streamdelete.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
package streaming
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *processor) StreamDelete(statusID string) error {
|
||||||
|
errs := []string{}
|
||||||
|
|
||||||
|
// we want to range through ALL streams for ALL accounts here to make sure it's very clear to everyone that the status has been deleted
|
||||||
|
p.streamMap.Range(func(k interface{}, v interface{}) bool {
|
||||||
|
// the key of this map should be an accountID (string)
|
||||||
|
accountID, ok := k.(string)
|
||||||
|
if !ok {
|
||||||
|
errs = append(errs, "key in streamMap was not a string!")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// the value of the map should be a buncha streams
|
||||||
|
streamsForAccount, ok := v.(*gtsmodel.StreamsForAccount)
|
||||||
|
if !ok {
|
||||||
|
errs = append(errs, fmt.Sprintf("stream map error for account stream %s", accountID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// lock the streams while we work on them
|
||||||
|
streamsForAccount.Lock()
|
||||||
|
defer streamsForAccount.Unlock()
|
||||||
|
for _, stream := range streamsForAccount.Streams {
|
||||||
|
// lock each individual stream as we work on it
|
||||||
|
stream.Lock()
|
||||||
|
defer stream.Unlock()
|
||||||
|
if stream.Connected {
|
||||||
|
stream.Messages <- >smodel.Message{
|
||||||
|
Stream: []string{stream.Type},
|
||||||
|
Event: "delete",
|
||||||
|
Payload: statusID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(errs) != 0 {
|
||||||
|
return fmt.Errorf("one or more errors streaming status delete: %s", strings.Join(errs, ";"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -18,9 +18,14 @@
|
||||||
type Processor interface {
|
type Processor interface {
|
||||||
// AuthorizeStreamingRequest returns an oauth2 token info in response to an access token query from the streaming API
|
// AuthorizeStreamingRequest returns an oauth2 token info in response to an access token query from the streaming API
|
||||||
AuthorizeStreamingRequest(accessToken string) (*gtsmodel.Account, error)
|
AuthorizeStreamingRequest(accessToken string) (*gtsmodel.Account, error)
|
||||||
|
// OpenStreamForAccount returns a new Stream for the given account, which will contain a channel for passing messages back to the caller.
|
||||||
OpenStreamForAccount(account *gtsmodel.Account, streamType string) (*gtsmodel.Stream, gtserror.WithCode)
|
OpenStreamForAccount(account *gtsmodel.Account, streamType string) (*gtsmodel.Stream, gtserror.WithCode)
|
||||||
|
// StreamStatusToAccount streams the given status to any open, appropriate streams belonging to the given account.
|
||||||
StreamStatusToAccount(s *apimodel.Status, account *gtsmodel.Account) error
|
StreamStatusToAccount(s *apimodel.Status, account *gtsmodel.Account) error
|
||||||
|
// StreamNotificationToAccount streams the given notification to any open, appropriate streams belonging to the given account.
|
||||||
StreamNotificationToAccount(n *apimodel.Notification, account *gtsmodel.Account) error
|
StreamNotificationToAccount(n *apimodel.Notification, account *gtsmodel.Account) error
|
||||||
|
// StreamDelete streams the delete of the given statusID to *ALL* open streams.
|
||||||
|
StreamDelete(statusID string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type processor struct {
|
type processor struct {
|
||||||
|
|
|
@ -74,11 +74,9 @@ type Manager interface {
|
||||||
GetOldestIndexedID(timelineAccountID string) (string, error)
|
GetOldestIndexedID(timelineAccountID string) (string, error)
|
||||||
// PrepareXFromTop prepares limit n amount of posts, based on their indexed representations, from the top of the index.
|
// PrepareXFromTop prepares limit n amount of posts, based on their indexed representations, from the top of the index.
|
||||||
PrepareXFromTop(timelineAccountID string, limit int) error
|
PrepareXFromTop(timelineAccountID string, limit int) error
|
||||||
// WipeStatusFromTimeline completely removes a status and from the index and prepared posts of the given account ID
|
// Remove removes one status from the timeline of the given timelineAccountID
|
||||||
//
|
Remove(statusID string, timelineAccountID string) (int, error)
|
||||||
// The returned int indicates how many entries were removed.
|
// WipeStatusFromAllTimelines removes one status from the index and prepared posts of all timelines
|
||||||
WipeStatusFromTimeline(timelineAccountID string, statusID string) (int, error)
|
|
||||||
// WipeStatusFromAllTimelines removes the status from the index and prepared posts of all timelines
|
|
||||||
WipeStatusFromAllTimelines(statusID string) error
|
WipeStatusFromAllTimelines(statusID string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,12 +175,6 @@ func (m *manager) PrepareXFromTop(timelineAccountID string, limit int) error {
|
||||||
return t.PrepareFromTop(limit)
|
return t.PrepareFromTop(limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *manager) WipeStatusFromTimeline(timelineAccountID string, statusID string) (int, error) {
|
|
||||||
t := m.getOrCreateTimeline(timelineAccountID)
|
|
||||||
|
|
||||||
return t.Remove(statusID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *manager) WipeStatusFromAllTimelines(statusID string) error {
|
func (m *manager) WipeStatusFromAllTimelines(statusID string) error {
|
||||||
errors := []string{}
|
errors := []string{}
|
||||||
m.accountTimelines.Range(func(k interface{}, i interface{}) bool {
|
m.accountTimelines.Range(func(k interface{}, i interface{}) bool {
|
||||||
|
@ -195,7 +187,7 @@ func (m *manager) WipeStatusFromAllTimelines(statusID string) error {
|
||||||
errors = append(errors, err.Error())
|
errors = append(errors, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|
|
@ -3,9 +3,16 @@
|
||||||
import (
|
import (
|
||||||
"container/list"
|
"container/list"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (t *timeline) Remove(statusID string) (int, error) {
|
func (t *timeline) Remove(statusID string) (int, error) {
|
||||||
|
l := t.log.WithFields(logrus.Fields{
|
||||||
|
"func": "Remove",
|
||||||
|
"accountTimeline": t.accountID,
|
||||||
|
"statusID": statusID,
|
||||||
|
})
|
||||||
t.Lock()
|
t.Lock()
|
||||||
defer t.Unlock()
|
defer t.Unlock()
|
||||||
var removed int
|
var removed int
|
||||||
|
@ -19,6 +26,7 @@ func (t *timeline) Remove(statusID string) (int, error) {
|
||||||
return removed, errors.New("Remove: could not parse e as a postIndexEntry")
|
return removed, errors.New("Remove: could not parse e as a postIndexEntry")
|
||||||
}
|
}
|
||||||
if entry.statusID == statusID {
|
if entry.statusID == statusID {
|
||||||
|
l.Debug("found status in postIndex")
|
||||||
removeIndexes = append(removeIndexes, e)
|
removeIndexes = append(removeIndexes, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,6 +45,7 @@ func (t *timeline) Remove(statusID string) (int, error) {
|
||||||
return removed, errors.New("Remove: could not parse e as a preparedPostsEntry")
|
return removed, errors.New("Remove: could not parse e as a preparedPostsEntry")
|
||||||
}
|
}
|
||||||
if entry.statusID == statusID {
|
if entry.statusID == statusID {
|
||||||
|
l.Debug("found status in preparedPosts")
|
||||||
removePrepared = append(removePrepared, e)
|
removePrepared = append(removePrepared, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,5 +55,6 @@ func (t *timeline) Remove(statusID string) (int, error) {
|
||||||
removed = removed + 1
|
removed = removed + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
l.Debugf("removed %d entries", removed)
|
||||||
return removed, nil
|
return removed, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -649,7 +649,7 @@ func (c *converter) BoostToAS(boostWrapperStatus *gtsmodel.Status, boostingAccou
|
||||||
if err := c.db.GetByID(boostWrapperStatus.BoostOfID, b); err != nil {
|
if err := c.db.GetByID(boostWrapperStatus.BoostOfID, b); err != nil {
|
||||||
return nil, fmt.Errorf("BoostToAS: error getting status with ID %s from the db: %s", boostWrapperStatus.BoostOfID, err)
|
return nil, fmt.Errorf("BoostToAS: error getting status with ID %s from the db: %s", boostWrapperStatus.BoostOfID, err)
|
||||||
}
|
}
|
||||||
boostWrapperStatus = b
|
boostWrapperStatus.GTSBoostedStatus = b
|
||||||
}
|
}
|
||||||
|
|
||||||
// create the announce
|
// create the announce
|
||||||
|
|
Loading…
Reference in a new issue