diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/api/client/status/status.go | 1 | ||||
| -rw-r--r-- | internal/api/client/status/statusunboost.go | 60 | ||||
| -rw-r--r-- | internal/processing/fromclientapi.go | 42 | ||||
| -rw-r--r-- | internal/processing/fromcommon.go | 6 | ||||
| -rw-r--r-- | internal/processing/processor.go | 2 | ||||
| -rw-r--r-- | internal/processing/status.go | 4 | ||||
| -rw-r--r-- | internal/processing/synchronous/status/status.go | 2 | ||||
| -rw-r--r-- | internal/processing/synchronous/status/unboost.go | 95 | ||||
| -rw-r--r-- | internal/processing/synchronous/streaming/streamdelete.go | 51 | ||||
| -rw-r--r-- | internal/processing/synchronous/streaming/streaming.go | 5 | ||||
| -rw-r--r-- | internal/timeline/manager.go | 16 | ||||
| -rw-r--r-- | internal/timeline/remove.go | 10 | ||||
| -rw-r--r-- | internal/typeutils/internaltoas.go | 2 | 
13 files changed, 282 insertions, 14 deletions
| diff --git a/internal/api/client/status/status.go b/internal/api/client/status/status.go index c5959edb0..dec6bebce 100644 --- a/internal/api/client/status/status.go +++ b/internal/api/client/status/status.go @@ -98,6 +98,7 @@ func (m *Module) Route(r router.Router) error {  	r.AttachHandler(http.MethodGet, FavouritedPath, m.StatusFavedByGETHandler)  	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, ContextPath, m.StatusContextGETHandler) diff --git a/internal/api/client/status/statusunboost.go b/internal/api/client/status/statusunboost.go new file mode 100644 index 000000000..cf6b61f52 --- /dev/null +++ b/internal/api/client/status/statusunboost.go @@ -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) +} diff --git a/internal/processing/fromclientapi.go b/internal/processing/fromclientapi.go index 8c4a1692e..180af04fe 100644 --- a/internal/processing/fromclientapi.go +++ b/internal/processing/fromclientapi.go @@ -138,6 +138,18 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error  				return errors.New("undo was not parseable as *gtsmodel.StatusFave")  			}  			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:  		// DELETE @@ -313,6 +325,36 @@ func (p *processor) federateUnfave(fave *gtsmodel.StatusFave, originAccount *gts  	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 {  	// if both accounts are local there's nothing to do here  	if originAccount.Domain == "" && targetAccount.Domain == "" { diff --git a/internal/processing/fromcommon.go b/internal/processing/fromcommon.go index e10f75441..d719b7f5f 100644 --- a/internal/processing/fromcommon.go +++ b/internal/processing/fromcommon.go @@ -401,5 +401,9 @@ func (p *processor) timelineStatusForAccount(status *gtsmodel.Status, accountID  }  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)  } diff --git a/internal/processing/processor.go b/internal/processing/processor.go index d1b44314d..618fd641b 100644 --- a/internal/processing/processor.go +++ b/internal/processing/processor.go @@ -117,6 +117,8 @@ type Processor interface {  	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(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(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. diff --git a/internal/processing/status.go b/internal/processing/status.go index 6848436d4..ab3843ded 100644 --- a/internal/processing/status.go +++ b/internal/processing/status.go @@ -40,6 +40,10 @@ func (p *processor) StatusBoost(authed *oauth.Auth, targetStatusID string) (*api  	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) {  	return p.statusProcessor.BoostedBy(authed.Account, targetStatusID)  } diff --git a/internal/processing/synchronous/status/status.go b/internal/processing/synchronous/status/status.go index cfc48ff30..d83c325fd 100644 --- a/internal/processing/synchronous/status/status.go +++ b/internal/processing/synchronous/status/status.go @@ -21,6 +21,8 @@ type Processor interface {  	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(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(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. diff --git a/internal/processing/synchronous/status/unboost.go b/internal/processing/synchronous/status/unboost.go new file mode 100644 index 000000000..2a1394695 --- /dev/null +++ b/internal/processing/synchronous/status/unboost.go @@ -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 +} diff --git a/internal/processing/synchronous/streaming/streamdelete.go b/internal/processing/synchronous/streaming/streamdelete.go new file mode 100644 index 000000000..2282c29ae --- /dev/null +++ b/internal/processing/synchronous/streaming/streamdelete.go @@ -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 +} diff --git a/internal/processing/synchronous/streaming/streaming.go b/internal/processing/synchronous/streaming/streaming.go index 9fd628547..de75b8f27 100644 --- a/internal/processing/synchronous/streaming/streaming.go +++ b/internal/processing/synchronous/streaming/streaming.go @@ -18,9 +18,14 @@ import (  type Processor interface {  	// AuthorizeStreamingRequest returns an oauth2 token info in response to an access token query from the streaming API  	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) +	// StreamStatusToAccount streams the given status to any open, appropriate streams belonging to the given account.  	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 +	// StreamDelete streams the delete of the given statusID to *ALL* open streams. +	StreamDelete(statusID string) error  }  type processor struct { diff --git a/internal/timeline/manager.go b/internal/timeline/manager.go index 2770f9e96..923fd010b 100644 --- a/internal/timeline/manager.go +++ b/internal/timeline/manager.go @@ -74,11 +74,9 @@ type Manager interface {  	GetOldestIndexedID(timelineAccountID string) (string, error)  	// PrepareXFromTop prepares limit n amount of posts, based on their indexed representations, from the top of the index.  	PrepareXFromTop(timelineAccountID string, limit int) error -	// WipeStatusFromTimeline completely removes a status and from the index and prepared posts of the given account ID -	// -	// The returned int indicates how many entries were removed. -	WipeStatusFromTimeline(timelineAccountID string, statusID string) (int, error) -	// WipeStatusFromAllTimelines removes the status from the index and prepared posts of all timelines +	// Remove removes one status from the timeline of the given timelineAccountID +	Remove(statusID string, timelineAccountID string) (int, error) +	// WipeStatusFromAllTimelines removes one status from the index and prepared posts of all timelines  	WipeStatusFromAllTimelines(statusID string) error  } @@ -177,12 +175,6 @@ func (m *manager) PrepareXFromTop(timelineAccountID string, limit int) error {  	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 {  	errors := []string{}  	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())  		} -		return false +		return true  	})  	var err error diff --git a/internal/timeline/remove.go b/internal/timeline/remove.go index 2f340d37b..8842c60cb 100644 --- a/internal/timeline/remove.go +++ b/internal/timeline/remove.go @@ -3,9 +3,16 @@ package timeline  import (  	"container/list"  	"errors" + +	"github.com/sirupsen/logrus"  )  func (t *timeline) Remove(statusID string) (int, error) { +	l := t.log.WithFields(logrus.Fields{ +		"func":            "Remove", +		"accountTimeline": t.accountID, +		"statusID":        statusID, +	})  	t.Lock()  	defer t.Unlock()  	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")  			}  			if entry.statusID == statusID { +				l.Debug("found status in postIndex")  				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")  			}  			if entry.statusID == statusID { +				l.Debug("found status in preparedPosts")  				removePrepared = append(removePrepared, e)  			}  		} @@ -46,5 +55,6 @@ func (t *timeline) Remove(statusID string) (int, error) {  		removed = removed + 1  	} +	l.Debugf("removed %d entries", removed)  	return removed, nil  } diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go index 296d0a182..bc7eee664 100644 --- a/internal/typeutils/internaltoas.go +++ b/internal/typeutils/internaltoas.go @@ -649,7 +649,7 @@ func (c *converter) BoostToAS(boostWrapperStatus *gtsmodel.Status, boostingAccou  		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)  		} -		boostWrapperStatus = b +		boostWrapperStatus.GTSBoostedStatus = b  	}  	// create the announce | 
