Block/unblock (#96)

* remote + local block logic, incl. federation

* improve blocking stuff

* fiddle with display of blocked profiles

* go fmt
This commit is contained in:
Tobi Smethurst 2021-07-11 16:22:21 +02:00 committed by GitHub
parent c7da64922f
commit 846057f0d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 1405 additions and 63 deletions

View file

@ -56,8 +56,8 @@ Things are moving on the project! As of July 2021 you can now:
* [ ] /api/v1/accounts/:id/identity_proofs GET (Get identity proofs for this account) * [ ] /api/v1/accounts/:id/identity_proofs GET (Get identity proofs for this account)
* [x] /api/v1/accounts/:id/follow POST (Follow this account) * [x] /api/v1/accounts/:id/follow POST (Follow this account)
* [x] /api/v1/accounts/:id/unfollow POST (Unfollow this account) * [x] /api/v1/accounts/:id/unfollow POST (Unfollow this account)
* [ ] /api/v1/accounts/:id/block POST (Block this account) * [x] /api/v1/accounts/:id/block POST (Block this account)
* [ ] /api/v1/accounts/:id/unblock POST (Unblock this account) * [x] /api/v1/accounts/:id/unblock POST (Unblock this account)
* [ ] /api/v1/accounts/:id/mute POST (Mute this account) * [ ] /api/v1/accounts/:id/mute POST (Mute this account)
* [ ] /api/v1/accounts/:id/unmute POST (Unmute this account) * [ ] /api/v1/accounts/:id/unmute POST (Unmute this account)
* [ ] /api/v1/accounts/:id/pin POST (Feature this account on profile) * [ ] /api/v1/accounts/:id/pin POST (Feature this account on profile)
@ -71,8 +71,8 @@ Things are moving on the project! As of July 2021 you can now:
* [x] /api/v1/favourites GET (See faved statuses) * [x] /api/v1/favourites GET (See faved statuses)
* [ ] Mutes * [ ] Mutes
* [ ] /api/v1/mutes GET (See list of muted accounts) * [ ] /api/v1/mutes GET (See list of muted accounts)
* [ ] Blocks * [x] Blocks
* [ ] /api/v1/blocks GET (See list of blocked accounts) * [x] /api/v1/blocks GET (See list of blocked accounts)
* [ ] Domain Blocks * [ ] Domain Blocks
* [x] /api/v1/domain_blocks GET (See list of domain blocks) * [x] /api/v1/domain_blocks GET (See list of domain blocks)
* [x] /api/v1/domain_blocks POST (Create a domain block) * [x] /api/v1/domain_blocks POST (Create a domain block)

View file

@ -61,10 +61,14 @@ const (
GetFollowingPath = BasePathWithID + "/following" GetFollowingPath = BasePathWithID + "/following"
// GetRelationshipsPath is for showing an account's relationship with other accounts // GetRelationshipsPath is for showing an account's relationship with other accounts
GetRelationshipsPath = BasePath + "/relationships" GetRelationshipsPath = BasePath + "/relationships"
// PostFollowPath is for POSTing new follows to, and updating existing follows // FollowPath is for POSTing new follows to, and updating existing follows
PostFollowPath = BasePathWithID + "/follow" FollowPath = BasePathWithID + "/follow"
// PostUnfollowPath is for POSTing an unfollow // UnfollowPath is for POSTing an unfollow
PostUnfollowPath = BasePathWithID + "/unfollow" UnfollowPath = BasePathWithID + "/unfollow"
// BlockPath is for creating a block of an account
BlockPath = BasePathWithID + "/block"
// UnblockPath is for removing a block of an account
UnblockPath = BasePathWithID + "/unblock"
) )
// Module implements the ClientAPIModule interface for account-related actions // Module implements the ClientAPIModule interface for account-related actions
@ -85,15 +89,33 @@ func New(config *config.Config, processor processing.Processor, log *logrus.Logg
// Route attaches all routes from this module to the given router // Route attaches all routes from this module to the given router
func (m *Module) Route(r router.Router) error { func (m *Module) Route(r router.Router) error {
// create account
r.AttachHandler(http.MethodPost, BasePath, m.AccountCreatePOSTHandler) r.AttachHandler(http.MethodPost, BasePath, m.AccountCreatePOSTHandler)
// get account
r.AttachHandler(http.MethodGet, BasePathWithID, m.muxHandler) r.AttachHandler(http.MethodGet, BasePathWithID, m.muxHandler)
// modify account
r.AttachHandler(http.MethodPatch, BasePathWithID, m.muxHandler) r.AttachHandler(http.MethodPatch, BasePathWithID, m.muxHandler)
// get account's statuses
r.AttachHandler(http.MethodGet, GetStatusesPath, m.AccountStatusesGETHandler) r.AttachHandler(http.MethodGet, GetStatusesPath, m.AccountStatusesGETHandler)
// get following or followers
r.AttachHandler(http.MethodGet, GetFollowersPath, m.AccountFollowersGETHandler) r.AttachHandler(http.MethodGet, GetFollowersPath, m.AccountFollowersGETHandler)
r.AttachHandler(http.MethodGet, GetFollowingPath, m.AccountFollowingGETHandler) r.AttachHandler(http.MethodGet, GetFollowingPath, m.AccountFollowingGETHandler)
// get relationship with account
r.AttachHandler(http.MethodGet, GetRelationshipsPath, m.AccountRelationshipsGETHandler) r.AttachHandler(http.MethodGet, GetRelationshipsPath, m.AccountRelationshipsGETHandler)
r.AttachHandler(http.MethodPost, PostFollowPath, m.AccountFollowPOSTHandler)
r.AttachHandler(http.MethodPost, PostUnfollowPath, m.AccountUnfollowPOSTHandler) // follow or unfollow account
r.AttachHandler(http.MethodPost, FollowPath, m.AccountFollowPOSTHandler)
r.AttachHandler(http.MethodPost, UnfollowPath, m.AccountUnfollowPOSTHandler)
// block or unblock account
r.AttachHandler(http.MethodPost, BlockPath, m.AccountBlockPOSTHandler)
r.AttachHandler(http.MethodPost, UnblockPath, m.AccountUnblockPOSTHandler)
return nil return nil
} }

View file

@ -0,0 +1,49 @@
/*
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 account
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
// AccountBlockPOSTHandler handles the creation of a block from the authed account targeting the given account ID.
func (m *Module) AccountBlockPOSTHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
targetAcctID := c.Param(IDKey)
if targetAcctID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
return
}
relationship, errWithCode := m.processor.AccountBlockCreate(authed, targetAcctID)
if errWithCode != nil {
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return
}
c.JSON(http.StatusOK, relationship)
}

View file

@ -0,0 +1,49 @@
/*
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 account
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
// AccountUnblockPOSTHandler handles the removal of a block from the authed account targeting the given account ID.
func (m *Module) AccountUnblockPOSTHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
targetAcctID := c.Param(IDKey)
if targetAcctID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
return
}
relationship, errWithCode := m.processor.AccountBlockRemove(authed, targetAcctID)
if errWithCode != nil {
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return
}
c.JSON(http.StatusOK, relationship)
}

View file

@ -0,0 +1,63 @@
/*
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 blocks
import (
"net/http"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/processing"
"github.com/superseriousbusiness/gotosocial/internal/router"
)
const (
// BasePath is the base URI path for serving favourites
BasePath = "/api/v1/blocks"
// MaxIDKey is the url query for setting a max ID to return
MaxIDKey = "max_id"
// SinceIDKey is the url query for returning results newer than the given ID
SinceIDKey = "since_id"
// LimitKey is for specifying maximum number of results to return.
LimitKey = "limit"
)
// Module implements the ClientAPIModule interface for everything relating to viewing blocks
type Module struct {
config *config.Config
processor processing.Processor
log *logrus.Logger
}
// New returns a new blocks module
func New(config *config.Config, processor processing.Processor, log *logrus.Logger) api.ClientModule {
return &Module{
config: config,
processor: processor,
log: log,
}
}
// 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.BlocksGETHandler)
return nil
}

View file

@ -0,0 +1,75 @@
/*
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 blocks
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
// BlocksGETHandler handles GETting blocks.
func (m *Module) BlocksGETHandler(c *gin.Context) {
l := m.log.WithField("func", "PublicTimelineGETHandler")
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
l.Debugf("error authing: %s", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
maxID := ""
maxIDString := c.Query(MaxIDKey)
if maxIDString != "" {
maxID = maxIDString
}
sinceID := ""
sinceIDString := c.Query(SinceIDKey)
if sinceIDString != "" {
sinceID = sinceIDString
}
limit := 20
limitString := c.Query(LimitKey)
if limitString != "" {
i, err := strconv.ParseInt(limitString, 10, 64)
if err != nil {
l.Debugf("error parsing limit string: %s", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"})
return
}
limit = int(i)
}
resp, errWithCode := m.processor.BlocksGet(authed, maxID, sinceID, limit)
if errWithCode != nil {
l.Debugf("error from processor BlocksGet: %s", errWithCode)
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
return
}
if resp.LinkHeader != "" {
c.Header("Link", resp.LinkHeader)
}
c.JSON(http.StatusOK, resp.Accounts)
}

View file

@ -67,23 +67,23 @@ sendLoop:
select { select {
case m := <-stream.Messages: case m := <-stream.Messages:
// we've got a streaming message!! // we've got a streaming message!!
l.Debug("received message from stream") l.Trace("received message from stream")
if err := conn.WriteJSON(m); err != nil { if err := conn.WriteJSON(m); err != nil {
l.Infof("error writing json to websocket connection: %s", err) l.Debugf("error writing json to websocket connection: %s", err)
// if something is wrong we want to bail and drop the connection -- the client will create a new one // if something is wrong we want to bail and drop the connection -- the client will create a new one
break sendLoop break sendLoop
} }
l.Debug("wrote message into websocket connection") l.Trace("wrote message into websocket connection")
case <-t.C: case <-t.C:
l.Debug("received TICK from ticker") l.Trace("received TICK from ticker")
if err := conn.WriteMessage(websocket.PingMessage, []byte(": ping")); err != nil { if err := conn.WriteMessage(websocket.PingMessage, []byte(": ping")); err != nil {
l.Infof("error writing ping to websocket connection: %s", err) l.Debugf("error writing ping to websocket connection: %s", err)
// if something is wrong we want to bail and drop the connection -- the client will create a new one // if something is wrong we want to bail and drop the connection -- the client will create a new one
break sendLoop break sendLoop
} }
l.Debug("wrote ping message into websocket connection") l.Trace("wrote ping message into websocket connection")
} }
} }
l.Debug("leaving StreamGETHandler") l.Trace("leaving StreamGETHandler")
} }

View file

@ -0,0 +1,26 @@
/*
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 model
// BlocksResponse wraps a slice of accounts, ready to be serialized, along with the Link
// header for the previous and next queries, to be returned to the client.
type BlocksResponse struct {
Accounts []*Account
LinkHeader string
}

View file

@ -14,6 +14,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/api/client/admin" "github.com/superseriousbusiness/gotosocial/internal/api/client/admin"
"github.com/superseriousbusiness/gotosocial/internal/api/client/app" "github.com/superseriousbusiness/gotosocial/internal/api/client/app"
"github.com/superseriousbusiness/gotosocial/internal/api/client/auth" "github.com/superseriousbusiness/gotosocial/internal/api/client/auth"
"github.com/superseriousbusiness/gotosocial/internal/api/client/blocks"
"github.com/superseriousbusiness/gotosocial/internal/api/client/emoji" "github.com/superseriousbusiness/gotosocial/internal/api/client/emoji"
"github.com/superseriousbusiness/gotosocial/internal/api/client/favourites" "github.com/superseriousbusiness/gotosocial/internal/api/client/favourites"
"github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver" "github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver"
@ -143,6 +144,7 @@ var Start cliactions.GTSAction = func(ctx context.Context, c *config.Config, log
securityModule := security.New(c, dbService, log) securityModule := security.New(c, dbService, log)
streamingModule := streaming.New(c, processor, log) streamingModule := streaming.New(c, processor, log)
favouritesModule := favourites.New(c, processor, log) favouritesModule := favourites.New(c, processor, log)
blocksModule := blocks.New(c, processor, log)
apis := []api.ClientModule{ apis := []api.ClientModule{
// modules with middleware go first // modules with middleware go first
@ -170,6 +172,7 @@ var Start cliactions.GTSAction = func(ctx context.Context, c *config.Config, log
listsModule, listsModule,
streamingModule, streamingModule,
favouritesModule, favouritesModule,
blocksModule,
} }
for _, m := range apis { for _, m := range apis {

View file

@ -16,6 +16,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/api/client/admin" "github.com/superseriousbusiness/gotosocial/internal/api/client/admin"
"github.com/superseriousbusiness/gotosocial/internal/api/client/app" "github.com/superseriousbusiness/gotosocial/internal/api/client/app"
"github.com/superseriousbusiness/gotosocial/internal/api/client/auth" "github.com/superseriousbusiness/gotosocial/internal/api/client/auth"
"github.com/superseriousbusiness/gotosocial/internal/api/client/blocks"
"github.com/superseriousbusiness/gotosocial/internal/api/client/emoji" "github.com/superseriousbusiness/gotosocial/internal/api/client/emoji"
"github.com/superseriousbusiness/gotosocial/internal/api/client/favourites" "github.com/superseriousbusiness/gotosocial/internal/api/client/favourites"
"github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver" "github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver"
@ -88,6 +89,7 @@ var Start cliactions.GTSAction = func(ctx context.Context, _ *config.Config, log
securityModule := security.New(c, dbService, log) securityModule := security.New(c, dbService, log)
streamingModule := streaming.New(c, processor, log) streamingModule := streaming.New(c, processor, log)
favouritesModule := favourites.New(c, processor, log) favouritesModule := favourites.New(c, processor, log)
blocksModule := blocks.New(c, processor, log)
apis := []api.ClientModule{ apis := []api.ClientModule{
// modules with middleware go first // modules with middleware go first
@ -115,6 +117,7 @@ var Start cliactions.GTSAction = func(ctx context.Context, _ *config.Config, log
listsModule, listsModule,
streamingModule, streamingModule,
favouritesModule, favouritesModule,
blocksModule,
} }
for _, m := range apis { for _, m := range apis {

View file

@ -159,6 +159,8 @@ type DB interface {
// In case of no entries, a 'no entries' error will be returned // In case of no entries, a 'no entries' error will be returned
GetStatusesForAccount(accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, error) GetStatusesForAccount(accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, error)
GetBlocksForAccount(accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, error)
// GetLastStatusForAccountID simply gets the most recent status by the given account. // GetLastStatusForAccountID simply gets the most recent status by the given account.
// The given slice 'status' pointer will be set to the result of the query, whatever it is. // The given slice 'status' pointer will be set to the result of the query, whatever it is.
// In case of no entries, a 'no entries' error will be returned // In case of no entries, a 'no entries' error will be returned

67
internal/db/pg/blocks.go Normal file
View file

@ -0,0 +1,67 @@
/*
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 pg
import (
"github.com/go-pg/pg/v10"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
func (ps *postgresService) GetBlocksForAccount(accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, error) {
blocks := []*gtsmodel.Block{}
fq := ps.conn.Model(&blocks).
Where("block.account_id = ?", accountID).
Relation("TargetAccount").
Order("block.id DESC")
if maxID != "" {
fq = fq.Where("block.id < ?", maxID)
}
if sinceID != "" {
fq = fq.Where("block.id > ?", sinceID)
}
if limit > 0 {
fq = fq.Limit(limit)
}
err := fq.Select()
if err != nil {
if err == pg.ErrNoRows {
return nil, "", "", db.ErrNoEntries{}
}
return nil, "", "", err
}
if len(blocks) == 0 {
return nil, "", "", db.ErrNoEntries{}
}
accounts := []*gtsmodel.Account{}
for _, b := range blocks {
accounts = append(accounts, b.TargetAccount)
}
nextMaxID := blocks[len(blocks)-1].ID
prevMinID := blocks[0].ID
return accounts, nextMaxID, prevMinID, nil
}

View file

@ -393,6 +393,7 @@ func (f *federator) DereferenceAnnounce(announce *gtsmodel.Status, requestingUse
announce.Language = boostedStatus.Language announce.Language = boostedStatus.Language
announce.Text = boostedStatus.Text announce.Text = boostedStatus.Text
announce.BoostOfID = boostedStatus.ID announce.BoostOfID = boostedStatus.ID
announce.BoostOfAccountID = boostedStatus.AccountID
announce.Visibility = boostedStatus.Visibility announce.Visibility = boostedStatus.Visibility
announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced
announce.GTSBoostedStatus = boostedStatus announce.GTSBoostedStatus = boostedStatus
@ -477,6 +478,7 @@ func (f *federator) DereferenceAnnounce(announce *gtsmodel.Status, requestingUse
announce.Language = boostedStatus.Language announce.Language = boostedStatus.Language
announce.Text = boostedStatus.Text announce.Text = boostedStatus.Text
announce.BoostOfID = boostedStatus.ID announce.BoostOfID = boostedStatus.ID
announce.BoostOfAccountID = boostedStatus.AccountID
announce.Visibility = boostedStatus.Visibility announce.Visibility = boostedStatus.Visibility
announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced
announce.GTSBoostedStatus = boostedStatus announce.GTSBoostedStatus = boostedStatus

View file

@ -129,6 +129,7 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
} }
} }
case gtsmodel.ActivityStreamsFollow: case gtsmodel.ActivityStreamsFollow:
// FOLLOW SOMETHING
follow, ok := asType.(vocab.ActivityStreamsFollow) follow, ok := asType.(vocab.ActivityStreamsFollow)
if !ok { if !ok {
return errors.New("could not convert type to follow") return errors.New("could not convert type to follow")
@ -156,6 +157,7 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
ReceivingAccount: targetAcct, ReceivingAccount: targetAcct,
} }
case gtsmodel.ActivityStreamsLike: case gtsmodel.ActivityStreamsLike:
// LIKE SOMETHING
like, ok := asType.(vocab.ActivityStreamsLike) like, ok := asType.(vocab.ActivityStreamsLike)
if !ok { if !ok {
return errors.New("could not convert type to like") return errors.New("could not convert type to like")
@ -182,6 +184,34 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
GTSModel: fave, GTSModel: fave,
ReceivingAccount: targetAcct, ReceivingAccount: targetAcct,
} }
case gtsmodel.ActivityStreamsBlock:
// BLOCK SOMETHING
blockable, ok := asType.(vocab.ActivityStreamsBlock)
if !ok {
return errors.New("could not convert type to block")
}
block, err := f.typeConverter.ASBlockToBlock(blockable)
if err != nil {
return fmt.Errorf("could not convert Block to gts model block")
}
newID, err := id.NewULID()
if err != nil {
return err
}
block.ID = newID
if err := f.db.Put(block); err != nil {
return fmt.Errorf("database error inserting block: %s", err)
}
fromFederatorChan <- gtsmodel.FromFederator{
APObjectType: gtsmodel.ActivityStreamsBlock,
APActivityType: gtsmodel.ActivityStreamsCreate,
GTSModel: block,
ReceivingAccount: targetAcct,
}
} }
return nil return nil
} }

View file

@ -39,16 +39,15 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {
"id": id.String(), "id": id.String(),
}, },
) )
l.Debugf("entering OWNS function with id %s", id.String()) l.Tracef("entering OWNS function with id %s", id.String())
// if the id host isn't this instance host, we don't own this IRI // if the id host isn't this instance host, we don't own this IRI
if id.Host != f.config.Host { if id.Host != f.config.Host {
l.Debugf("we DO NOT own activity because the host is %s not %s", id.Host, f.config.Host) l.Tracef("we DO NOT own activity because the host is %s not %s", id.Host, f.config.Host)
return false, nil return false, nil
} }
// apparently it belongs to this host, so what *is* it? // apparently it belongs to this host, so what *is* it?
// check if it's a status, eg /users/example_username/statuses/SOME_UUID_OF_A_STATUS // check if it's a status, eg /users/example_username/statuses/SOME_UUID_OF_A_STATUS
if util.IsStatusesPath(id) { if util.IsStatusesPath(id) {
_, uid, err := util.ParseStatusesPath(id) _, uid, err := util.ParseStatusesPath(id)
@ -63,11 +62,10 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {
// an actual error happened // an actual error happened
return false, fmt.Errorf("database error fetching status with id %s: %s", uid, err) return false, fmt.Errorf("database error fetching status with id %s: %s", uid, err)
} }
l.Debug("we DO own this") l.Debugf("we own url %s", id.String())
return true, nil return true, nil
} }
// check if it's a user, eg /users/example_username
if util.IsUserPath(id) { if util.IsUserPath(id) {
username, err := util.ParseUserPath(id) username, err := util.ParseUserPath(id)
if err != nil { if err != nil {
@ -81,7 +79,7 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {
// an actual error happened // an actual error happened
return false, fmt.Errorf("database error fetching account with username %s: %s", username, err) return false, fmt.Errorf("database error fetching account with username %s: %s", username, err)
} }
l.Debug("we DO own this") l.Debugf("we own url %s", id.String())
return true, nil return true, nil
} }
@ -98,7 +96,7 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {
// an actual error happened // an actual error happened
return false, fmt.Errorf("database error fetching account with username %s: %s", username, err) return false, fmt.Errorf("database error fetching account with username %s: %s", username, err)
} }
l.Debug("we DO own this") l.Debugf("we own url %s", id.String())
return true, nil return true, nil
} }
@ -115,7 +113,57 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {
// an actual error happened // an actual error happened
return false, fmt.Errorf("database error fetching account with username %s: %s", username, err) return false, fmt.Errorf("database error fetching account with username %s: %s", username, err)
} }
l.Debug("we DO own this") l.Debugf("we own url %s", id.String())
return true, nil
}
if util.IsLikePath(id) {
username, likeID, err := util.ParseLikedPath(id)
if err != nil {
return false, fmt.Errorf("error parsing like path for url %s: %s", id.String(), err)
}
if err := f.db.GetLocalAccountByUsername(username, &gtsmodel.Account{}); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
// there are no entries for this username
return false, nil
}
// an actual error happened
return false, fmt.Errorf("database error fetching account with username %s: %s", username, err)
}
if err := f.db.GetByID(likeID, &gtsmodel.StatusFave{}); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
// there are no entries
return false, nil
}
// an actual error happened
return false, fmt.Errorf("database error fetching like with id %s: %s", likeID, err)
}
l.Debugf("we own url %s", id.String())
return true, nil
}
if util.IsBlockPath(id) {
username, blockID, err := util.ParseBlockPath(id)
if err != nil {
return false, fmt.Errorf("error parsing block path for url %s: %s", id.String(), err)
}
if err := f.db.GetLocalAccountByUsername(username, &gtsmodel.Account{}); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
// there are no entries for this username
return false, nil
}
// an actual error happened
return false, fmt.Errorf("database error fetching account with username %s: %s", username, err)
}
if err := f.db.GetByID(blockID, &gtsmodel.Block{}); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
// there are no entries
return false, nil
}
// an actual error happened
return false, fmt.Errorf("database error fetching block with id %s: %s", blockID, err)
}
l.Debugf("we own url %s", id.String())
return true, nil return true, nil
} }

View file

@ -85,6 +85,31 @@ func (f *federatingDB) Undo(ctx context.Context, undo vocab.ActivityStreamsUndo)
// UNDO LIKE // UNDO LIKE
case string(gtsmodel.ActivityStreamsAnnounce): case string(gtsmodel.ActivityStreamsAnnounce):
// UNDO BOOST/REBLOG/ANNOUNCE // UNDO BOOST/REBLOG/ANNOUNCE
case string(gtsmodel.ActivityStreamsBlock):
// UNDO BLOCK
ASBlock, ok := iter.GetType().(vocab.ActivityStreamsBlock)
if !ok {
return errors.New("UNDO: couldn't parse block into vocab.ActivityStreamsBlock")
}
// make sure the actor owns the follow
if !sameActor(undo.GetActivityStreamsActor(), ASBlock.GetActivityStreamsActor()) {
return errors.New("UNDO: block actor and activity actor not the same")
}
// convert the block to something we can understand
gtsBlock, err := f.typeConverter.ASBlockToBlock(ASBlock)
if err != nil {
return fmt.Errorf("UNDO: error converting asblock to gtsblock: %s", err)
}
// make sure the addressee of the original block is the same as whatever inbox this landed in
if gtsBlock.TargetAccountID != targetAcct.ID {
return errors.New("UNDO: block object account and inbox account were not the same")
}
// delete any existing BLOCK
if err := f.db.DeleteWhere([]db.Where{{Key: "uri", Value: gtsBlock.URI}}, &gtsmodel.Block{}); err != nil {
return fmt.Errorf("UNDO: db error removing block: %s", err)
}
l.Debug("block undone")
return nil
} }
} }

View file

@ -139,7 +139,7 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (idURL *url.URL, e
// ID might already be set on an announce we've created, so check it here and return it if it is // ID might already be set on an announce we've created, so check it here and return it if it is
announce, ok := t.(vocab.ActivityStreamsAnnounce) announce, ok := t.(vocab.ActivityStreamsAnnounce)
if !ok { if !ok {
return nil, errors.New("newid: fave couldn't be parsed into vocab.ActivityStreamsAnnounce") return nil, errors.New("newid: announce couldn't be parsed into vocab.ActivityStreamsAnnounce")
} }
idProp := announce.GetJSONLDId() idProp := announce.GetJSONLDId()
if idProp != nil { if idProp != nil {
@ -152,7 +152,7 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (idURL *url.URL, e
// ID might already be set on an update we've created, so check it here and return it if it is // ID might already be set on an update we've created, so check it here and return it if it is
update, ok := t.(vocab.ActivityStreamsUpdate) update, ok := t.(vocab.ActivityStreamsUpdate)
if !ok { if !ok {
return nil, errors.New("newid: fave couldn't be parsed into vocab.ActivityStreamsUpdate") return nil, errors.New("newid: update couldn't be parsed into vocab.ActivityStreamsUpdate")
} }
idProp := update.GetJSONLDId() idProp := update.GetJSONLDId()
if idProp != nil { if idProp != nil {
@ -160,6 +160,32 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (idURL *url.URL, e
return idProp.GetIRI(), nil return idProp.GetIRI(), nil
} }
} }
case gtsmodel.ActivityStreamsBlock:
// BLOCK
// ID might already be set on a block we've created, so check it here and return it if it is
block, ok := t.(vocab.ActivityStreamsBlock)
if !ok {
return nil, errors.New("newid: block couldn't be parsed into vocab.ActivityStreamsBlock")
}
idProp := block.GetJSONLDId()
if idProp != nil {
if idProp.IsIRI() {
return idProp.GetIRI(), nil
}
}
case gtsmodel.ActivityStreamsUndo:
// UNDO
// ID might already be set on an undo we've created, so check it here and return it if it is
undo, ok := t.(vocab.ActivityStreamsUndo)
if !ok {
return nil, errors.New("newid: undo couldn't be parsed into vocab.ActivityStreamsUndo")
}
idProp := undo.GetJSONLDId()
if idProp != nil {
if idProp.IsIRI() {
return idProp.GetIRI(), nil
}
}
} }
// fallback default behavior: just return a random ULID after our protocol and host // fallback default behavior: just return a random ULID after our protocol and host

View file

@ -243,8 +243,8 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er
return true, nil return true, nil
} }
a := &gtsmodel.Account{} requestingAccount := &gtsmodel.Account{}
if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, a); err != nil { if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, requestingAccount); err != nil {
_, ok := err.(db.ErrNoEntries) _, ok := err.(db.ErrNoEntries)
if ok { if ok {
// we don't have an entry for this account so it's not blocked // we don't have an entry for this account so it's not blocked
@ -253,11 +253,13 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er
} }
return false, fmt.Errorf("error getting account with uri %s: %s", uri.String(), err) return false, fmt.Errorf("error getting account with uri %s: %s", uri.String(), err)
} }
blocked, err := f.db.Blocked(requestedAccount.ID, a.ID)
if err != nil { // check if requested account blocks requesting account
return false, fmt.Errorf("error checking account blocks: %s", err) if err := f.db.GetWhere([]db.Where{
} {Key: "account_id", Value: requestedAccount.ID},
if blocked { {Key: "target_account_id", Value: requestingAccount.ID},
}, &gtsmodel.Block{}); err == nil {
// a block exists
return true, nil return true, nil
} }
} }

View file

@ -11,9 +11,11 @@ type Block struct {
// When was this block updated // When was this block updated
UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
// Who created this block? // Who created this block?
AccountID string `pg:"type:CHAR(26),notnull"` AccountID string `pg:"type:CHAR(26),notnull"`
Account *Account `pg:"rel:has-one"`
// Who is targeted by this block? // Who is targeted by this block?
TargetAccountID string `pg:"type:CHAR(26),notnull"` TargetAccountID string `pg:"type:CHAR(26),notnull"`
TargetAccount *Account `pg:"rel:has-one"`
// Activitypub URI for this block // Activitypub URI for this block
URI string URI string `pg:",notnull"`
} }

View file

@ -56,6 +56,8 @@ type Status struct {
InReplyToAccountID string `pg:"type:CHAR(26)"` InReplyToAccountID string `pg:"type:CHAR(26)"`
// id of the status this status is a boost of // id of the status this status is a boost of
BoostOfID string `pg:"type:CHAR(26)"` BoostOfID string `pg:"type:CHAR(26)"`
// id of the account that owns the boosted status
BoostOfAccountID string `pg:"type:CHAR(26)"`
// cw string for this status // cw string for this status
ContentWarning string ContentWarning string
// visibility entry for this status // visibility entry for this status

View file

@ -59,3 +59,11 @@ func (p *processor) AccountFollowCreate(authed *oauth.Auth, form *apimodel.Accou
func (p *processor) AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { func (p *processor) AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
return p.accountProcessor.FollowRemove(authed.Account, targetAccountID) return p.accountProcessor.FollowRemove(authed.Account, targetAccountID)
} }
func (p *processor) AccountBlockCreate(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
return p.accountProcessor.BlockCreate(authed.Account, targetAccountID)
}
func (p *processor) AccountBlockRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
return p.accountProcessor.BlockRemove(authed.Account, targetAccountID)
}

View file

@ -59,6 +59,11 @@ type Processor interface {
FollowCreate(requestingAccount *gtsmodel.Account, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode) FollowCreate(requestingAccount *gtsmodel.Account, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode)
// FollowRemove handles the removal of a follow/follow request to an account, either remote or local. // FollowRemove handles the removal of a follow/follow request to an account, either remote or local.
FollowRemove(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) FollowRemove(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
// BlockCreate handles the creation of a block from requestingAccount to targetAccountID, either remote or local.
BlockCreate(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
// BlockRemove handles the removal of a block from requestingAccount to targetAccountID, either remote or local.
BlockRemove(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
// UpdateHeader does the dirty work of checking the header part of an account update form, // UpdateHeader does the dirty work of checking the header part of an account update form,
// parsing and checking the image, and doing the necessary updates in the database for this to become // parsing and checking the image, and doing the necessary updates in the database for this to become
// the account's new header image. // the account's new header image.

View file

@ -0,0 +1,155 @@
/*
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 account
import (
"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"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
func (p *processor) BlockCreate(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
// make sure the target account actually exists in our db
targetAcct := &gtsmodel.Account{}
if err := p.db.GetByID(targetAccountID, targetAcct); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("BlockCreate: account %s not found in the db: %s", targetAccountID, err))
}
}
// if requestingAccount already blocks target account, we don't need to do anything
block := &gtsmodel.Block{}
if err := p.db.GetWhere([]db.Where{
{Key: "account_id", Value: requestingAccount.ID},
{Key: "target_account_id", Value: targetAccountID},
}, block); err == nil {
// block already exists, just return relationship
return p.RelationshipGet(requestingAccount, targetAccountID)
}
// make the block
newBlockID, err := id.NewULID()
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
block.ID = newBlockID
block.AccountID = requestingAccount.ID
block.Account = requestingAccount
block.TargetAccountID = targetAccountID
block.TargetAccount = targetAcct
block.URI = util.GenerateURIForBlock(requestingAccount.Username, p.config.Protocol, p.config.Host, newBlockID)
// whack it in the database
if err := p.db.Put(block); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error creating block in db: %s", err))
}
// clear any follows or follow requests from the blocked account to the target account -- this is a simple delete
if err := p.db.DeleteWhere([]db.Where{
{Key: "account_id", Value: targetAccountID},
{Key: "target_account_id", Value: requestingAccount.ID},
}, &gtsmodel.Follow{}); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error removing follow in db: %s", err))
}
if err := p.db.DeleteWhere([]db.Where{
{Key: "account_id", Value: targetAccountID},
{Key: "target_account_id", Value: requestingAccount.ID},
}, &gtsmodel.FollowRequest{}); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error removing follow in db: %s", err))
}
// clear any follows or follow requests from the requesting account to the target account --
// this might require federation so we need to pass some messages around
// check if a follow request exists from the requesting account to the target account, and remove it if it does (storing the URI for later)
var frChanged bool
var frURI string
fr := &gtsmodel.FollowRequest{}
if err := p.db.GetWhere([]db.Where{
{Key: "account_id", Value: requestingAccount.ID},
{Key: "target_account_id", Value: targetAccountID},
}, fr); err == nil {
frURI = fr.URI
if err := p.db.DeleteByID(fr.ID, fr); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error removing follow request from db: %s", err))
}
frChanged = true
}
// now do the same thing for any existing follow
var fChanged bool
var fURI string
f := &gtsmodel.Follow{}
if err := p.db.GetWhere([]db.Where{
{Key: "account_id", Value: requestingAccount.ID},
{Key: "target_account_id", Value: targetAccountID},
}, f); err == nil {
fURI = f.URI
if err := p.db.DeleteByID(f.ID, f); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error removing follow from db: %s", err))
}
fChanged = true
}
// follow request status changed so send the UNDO activity to the channel for async processing
if frChanged {
p.fromClientAPI <- gtsmodel.FromClientAPI{
APObjectType: gtsmodel.ActivityStreamsFollow,
APActivityType: gtsmodel.ActivityStreamsUndo,
GTSModel: &gtsmodel.Follow{
AccountID: requestingAccount.ID,
TargetAccountID: targetAccountID,
URI: frURI,
},
OriginAccount: requestingAccount,
TargetAccount: targetAcct,
}
}
// follow status changed so send the UNDO activity to the channel for async processing
if fChanged {
p.fromClientAPI <- gtsmodel.FromClientAPI{
APObjectType: gtsmodel.ActivityStreamsFollow,
APActivityType: gtsmodel.ActivityStreamsUndo,
GTSModel: &gtsmodel.Follow{
AccountID: requestingAccount.ID,
TargetAccountID: targetAccountID,
URI: fURI,
},
OriginAccount: requestingAccount,
TargetAccount: targetAcct,
}
}
// handle the rest of the block process asynchronously
p.fromClientAPI <- gtsmodel.FromClientAPI{
APObjectType: gtsmodel.ActivityStreamsBlock,
APActivityType: gtsmodel.ActivityStreamsCreate,
GTSModel: block,
OriginAccount: requestingAccount,
TargetAccount: targetAcct,
}
return p.RelationshipGet(requestingAccount, targetAccountID)
}

View file

@ -45,9 +45,19 @@ func (p *processor) Get(requestingAccount *gtsmodel.Account, targetAccountID str
p.log.WithField("func", "AccountGet").Debugf("dereferencing account: %s", err) p.log.WithField("func", "AccountGet").Debugf("dereferencing account: %s", err)
} }
var mastoAccount *apimodel.Account var blocked bool
var err error var err error
if requestingAccount != nil && targetAccount.ID == requestingAccount.ID { if requestingAccount != nil {
blocked, err = p.db.Blocked(requestingAccount.ID, targetAccountID)
if err != nil {
return nil, fmt.Errorf("error checking account block: %s", err)
}
}
var mastoAccount *apimodel.Account
if blocked {
mastoAccount, err = p.tc.AccountToMastoBlocked(targetAccount)
} else if requestingAccount != nil && targetAccount.ID == requestingAccount.ID {
mastoAccount, err = p.tc.AccountToMastoSensitive(targetAccount) mastoAccount, err = p.tc.AccountToMastoSensitive(targetAccount)
} else { } else {
mastoAccount, err = p.tc.AccountToMastoPublic(targetAccount) mastoAccount, err = p.tc.AccountToMastoPublic(targetAccount)

View file

@ -0,0 +1,67 @@
/*
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 account
import (
"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) BlockRemove(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
// make sure the target account actually exists in our db
targetAcct := &gtsmodel.Account{}
if err := p.db.GetByID(targetAccountID, targetAcct); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("BlockRemove: account %s not found in the db: %s", targetAccountID, err))
}
}
// check if a block exists, and remove it if it does (storing the URI for later)
var blockChanged bool
block := &gtsmodel.Block{}
if err := p.db.GetWhere([]db.Where{
{Key: "account_id", Value: requestingAccount.ID},
{Key: "target_account_id", Value: targetAccountID},
}, block); err == nil {
block.Account = requestingAccount
block.TargetAccount = targetAcct
if err := p.db.DeleteByID(block.ID, &gtsmodel.Block{}); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockRemove: error removing block from db: %s", err))
}
blockChanged = true
}
// block status changed so send the UNDO activity to the channel for async processing
if blockChanged {
p.fromClientAPI <- gtsmodel.FromClientAPI{
APObjectType: gtsmodel.ActivityStreamsBlock,
APActivityType: gtsmodel.ActivityStreamsUndo,
GTSModel: block,
OriginAccount: requestingAccount,
TargetAccount: targetAcct,
}
}
// return whatever relationship results from all this
return p.RelationshipGet(requestingAccount, targetAccountID)
}

View file

@ -0,0 +1,83 @@
/*
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 processing
import (
"fmt"
"net/url"
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/oauth"
)
func (p *processor) BlocksGet(authed *oauth.Auth, maxID string, sinceID string, limit int) (*apimodel.BlocksResponse, gtserror.WithCode) {
accounts, nextMaxID, prevMinID, err := p.db.GetBlocksForAccount(authed.Account.ID, maxID, sinceID, limit)
if err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
// there are just no entries
return &apimodel.BlocksResponse{
Accounts: []*apimodel.Account{},
}, nil
}
// there's an actual error
return nil, gtserror.NewErrorInternalError(err)
}
apiAccounts := []*apimodel.Account{}
for _, a := range accounts {
apiAccount, err := p.tc.AccountToMastoBlocked(a)
if err != nil {
continue
}
apiAccounts = append(apiAccounts, apiAccount)
}
return p.packageBlocksResponse(apiAccounts, "/api/v1/blocks", nextMaxID, prevMinID, limit)
}
func (p *processor) packageBlocksResponse(accounts []*apimodel.Account, path string, nextMaxID string, prevMinID string, limit int) (*apimodel.BlocksResponse, gtserror.WithCode) {
resp := &apimodel.BlocksResponse{
Accounts: []*apimodel.Account{},
}
resp.Accounts = accounts
// prepare the next and previous links
if len(accounts) != 0 {
nextLink := &url.URL{
Scheme: p.config.Protocol,
Host: p.config.Host,
Path: path,
RawQuery: fmt.Sprintf("limit=%d&max_id=%s", limit, nextMaxID),
}
next := fmt.Sprintf("<%s>; rel=\"next\"", nextLink.String())
prevLink := &url.URL{
Scheme: p.config.Protocol,
Host: p.config.Host,
Path: path,
RawQuery: fmt.Sprintf("limit=%d&min_id=%s", limit, prevMinID),
}
prev := fmt.Sprintf("<%s>; rel=\"prev\"", prevLink.String())
resp.LinkHeader = fmt.Sprintf("%s, %s", next, prev)
}
return resp, nil
}

View file

@ -76,7 +76,6 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error
} }
return p.federateFave(fave, clientMsg.OriginAccount, clientMsg.TargetAccount) return p.federateFave(fave, clientMsg.OriginAccount, clientMsg.TargetAccount)
case gtsmodel.ActivityStreamsAnnounce: case gtsmodel.ActivityStreamsAnnounce:
// CREATE BOOST/ANNOUNCE // CREATE BOOST/ANNOUNCE
boostWrapperStatus, ok := clientMsg.GTSModel.(*gtsmodel.Status) boostWrapperStatus, ok := clientMsg.GTSModel.(*gtsmodel.Status)
@ -93,6 +92,25 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error
} }
return p.federateAnnounce(boostWrapperStatus, clientMsg.OriginAccount, clientMsg.TargetAccount) return p.federateAnnounce(boostWrapperStatus, clientMsg.OriginAccount, clientMsg.TargetAccount)
case gtsmodel.ActivityStreamsBlock:
// CREATE BLOCK
block, ok := clientMsg.GTSModel.(*gtsmodel.Block)
if !ok {
return errors.New("block was not parseable as *gtsmodel.Block")
}
// remove any of the blocking account's statuses from the blocked account's timeline, and vice versa
if err := p.timelineManager.WipeStatusesFromAccountID(block.AccountID, block.TargetAccountID); err != nil {
return err
}
if err := p.timelineManager.WipeStatusesFromAccountID(block.TargetAccountID, block.AccountID); err != nil {
return err
}
// TODO: same with notifications
// TODO: same with bookmarks
return p.federateBlock(block)
} }
case gtsmodel.ActivityStreamsUpdate: case gtsmodel.ActivityStreamsUpdate:
// UPDATE // UPDATE
@ -132,6 +150,13 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error
return errors.New("undo was not parseable as *gtsmodel.Follow") return errors.New("undo was not parseable as *gtsmodel.Follow")
} }
return p.federateUnfollow(follow, clientMsg.OriginAccount, clientMsg.TargetAccount) return p.federateUnfollow(follow, clientMsg.OriginAccount, clientMsg.TargetAccount)
case gtsmodel.ActivityStreamsBlock:
// UNDO BLOCK
block, ok := clientMsg.GTSModel.(*gtsmodel.Block)
if !ok {
return errors.New("undo was not parseable as *gtsmodel.Block")
}
return p.federateUnblock(block)
case gtsmodel.ActivityStreamsLike: case gtsmodel.ActivityStreamsLike:
// UNDO LIKE/FAVE // UNDO LIKE/FAVE
fave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave) fave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave)
@ -530,3 +555,93 @@ func (p *processor) federateAccountUpdate(updatedAccount *gtsmodel.Account, orig
_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, update) _, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, update)
return err return err
} }
func (p *processor) federateBlock(block *gtsmodel.Block) error {
if block.Account == nil {
a := &gtsmodel.Account{}
if err := p.db.GetByID(block.AccountID, a); err != nil {
return fmt.Errorf("federateBlock: error getting block account from database: %s", err)
}
block.Account = a
}
if block.TargetAccount == nil {
a := &gtsmodel.Account{}
if err := p.db.GetByID(block.TargetAccountID, a); err != nil {
return fmt.Errorf("federateBlock: error getting block target account from database: %s", err)
}
block.TargetAccount = a
}
// if both accounts are local there's nothing to do here
if block.Account.Domain == "" && block.TargetAccount.Domain == "" {
return nil
}
asBlock, err := p.tc.BlockToAS(block)
if err != nil {
return fmt.Errorf("federateBlock: error converting block to AS format: %s", err)
}
outboxIRI, err := url.Parse(block.Account.OutboxURI)
if err != nil {
return fmt.Errorf("federateBlock: error parsing outboxURI %s: %s", block.Account.OutboxURI, err)
}
_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, asBlock)
return err
}
func (p *processor) federateUnblock(block *gtsmodel.Block) error {
if block.Account == nil {
a := &gtsmodel.Account{}
if err := p.db.GetByID(block.AccountID, a); err != nil {
return fmt.Errorf("federateUnblock: error getting block account from database: %s", err)
}
block.Account = a
}
if block.TargetAccount == nil {
a := &gtsmodel.Account{}
if err := p.db.GetByID(block.TargetAccountID, a); err != nil {
return fmt.Errorf("federateUnblock: error getting block target account from database: %s", err)
}
block.TargetAccount = a
}
// if both accounts are local there's nothing to do here
if block.Account.Domain == "" && block.TargetAccount.Domain == "" {
return nil
}
asBlock, err := p.tc.BlockToAS(block)
if err != nil {
return fmt.Errorf("federateUnblock: error converting block to AS format: %s", err)
}
targetAccountURI, err := url.Parse(block.TargetAccount.URI)
if err != nil {
return fmt.Errorf("federateUnblock: error parsing uri %s: %s", block.TargetAccount.URI, err)
}
// create an Undo and set the appropriate actor on it
undo := streams.NewActivityStreamsUndo()
undo.SetActivityStreamsActor(asBlock.GetActivityStreamsActor())
// Set the block as the 'object' property.
undoObject := streams.NewActivityStreamsObjectProperty()
undoObject.AppendActivityStreamsBlock(asBlock)
undo.SetActivityStreamsObject(undoObject)
// Set the To of the undo as the target of the block
undoTo := streams.NewActivityStreamsToProperty()
undoTo.AppendIRI(targetAccountURI)
undo.SetActivityStreamsTo(undoTo)
outboxIRI, err := url.Parse(block.Account.OutboxURI)
if err != nil {
return fmt.Errorf("federateUnblock: error parsing outboxURI %s: %s", block.Account.OutboxURI, err)
}
_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, undo)
return err
}

View file

@ -34,7 +34,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
"federatorMsg": fmt.Sprintf("%+v", federatorMsg), "federatorMsg": fmt.Sprintf("%+v", federatorMsg),
}) })
l.Debug("entering function PROCESS FROM FEDERATOR") l.Trace("entering function PROCESS FROM FEDERATOR")
switch federatorMsg.APActivityType { switch federatorMsg.APActivityType {
case gtsmodel.ActivityStreamsCreate: case gtsmodel.ActivityStreamsCreate:
@ -47,7 +47,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
return errors.New("note was not parseable as *gtsmodel.Status") return errors.New("note was not parseable as *gtsmodel.Status")
} }
l.Debug("will now derefence incoming status") l.Trace("will now derefence incoming status")
if err := p.federator.DereferenceStatusFields(incomingStatus, federatorMsg.ReceivingAccount.Username); err != nil { if err := p.federator.DereferenceStatusFields(incomingStatus, federatorMsg.ReceivingAccount.Username); err != nil {
return fmt.Errorf("error dereferencing status from federator: %s", err) return fmt.Errorf("error dereferencing status from federator: %s", err)
} }
@ -70,7 +70,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
return errors.New("profile was not parseable as *gtsmodel.Account") return errors.New("profile was not parseable as *gtsmodel.Account")
} }
l.Debug("will now derefence incoming account") l.Trace("will now derefence incoming account")
if err := p.federator.DereferenceAccountFields(incomingAccount, "", false); err != nil { if err := p.federator.DereferenceAccountFields(incomingAccount, "", false); err != nil {
return fmt.Errorf("error dereferencing account from federator: %s", err) return fmt.Errorf("error dereferencing account from federator: %s", err)
} }
@ -127,6 +127,22 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
if err := p.notifyAnnounce(incomingAnnounce); err != nil { if err := p.notifyAnnounce(incomingAnnounce); err != nil {
return err return err
} }
case gtsmodel.ActivityStreamsBlock:
// CREATE A BLOCK
block, ok := federatorMsg.GTSModel.(*gtsmodel.Block)
if !ok {
return errors.New("block was not parseable as *gtsmodel.Block")
}
// remove any of the blocking account's statuses from the blocked account's timeline, and vice versa
if err := p.timelineManager.WipeStatusesFromAccountID(block.AccountID, block.TargetAccountID); err != nil {
return err
}
if err := p.timelineManager.WipeStatusesFromAccountID(block.TargetAccountID, block.AccountID); err != nil {
return err
}
// TODO: same with notifications
// TODO: same with bookmarks
} }
case gtsmodel.ActivityStreamsUpdate: case gtsmodel.ActivityStreamsUpdate:
// UPDATE // UPDATE
@ -138,7 +154,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
return errors.New("profile was not parseable as *gtsmodel.Account") return errors.New("profile was not parseable as *gtsmodel.Account")
} }
l.Debug("will now derefence incoming account") l.Trace("will now derefence incoming account")
if err := p.federator.DereferenceAccountFields(incomingAccount, federatorMsg.ReceivingAccount.Username, true); err != nil { if err := p.federator.DereferenceAccountFields(incomingAccount, federatorMsg.ReceivingAccount.Username, true); err != nil {
return fmt.Errorf("error dereferencing account from federator: %s", err) return fmt.Errorf("error dereferencing account from federator: %s", err)
} }

View file

@ -82,6 +82,10 @@ type Processor interface {
AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode) AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode)
// AccountFollowRemove handles the removal of a follow/follow request to an account, either remote or local. // AccountFollowRemove handles the removal of a follow/follow request to an account, either remote or local.
AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
// AccountBlockCreate handles the creation of a block from authed account to target account, either remote or local.
AccountBlockCreate(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
// AccountBlockRemove handles the removal of a block from authed account to target account, either remote or local.
AccountBlockRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
// AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form. // AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form.
AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error)
@ -99,6 +103,9 @@ type Processor interface {
// AppCreate processes the creation of a new API application // AppCreate processes the creation of a new API application
AppCreate(authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, error) AppCreate(authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, error)
// BlocksGet returns a list of accounts blocked by the requesting account.
BlocksGet(authed *oauth.Auth, maxID string, sinceID string, limit int) (*apimodel.BlocksResponse, 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(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) FileGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error)
@ -275,14 +282,14 @@ func (p *processor) Start() error {
for { for {
select { select {
case clientMsg := <-p.fromClientAPI: case clientMsg := <-p.fromClientAPI:
p.log.Infof("received message FROM client API: %+v", clientMsg) p.log.Tracef("received message FROM client API: %+v", clientMsg)
go func() { go func() {
if err := p.processFromClientAPI(clientMsg); err != nil { if err := p.processFromClientAPI(clientMsg); err != nil {
p.log.Error(err) p.log.Error(err)
} }
}() }()
case federatorMsg := <-p.fromFederator: case federatorMsg := <-p.fromFederator:
p.log.Infof("received message FROM federator: %+v", federatorMsg) p.log.Tracef("received message FROM federator: %+v", federatorMsg)
go func() { go func() {
if err := p.processFromFederator(federatorMsg); err != nil { if err := p.processFromFederator(federatorMsg); err != nil {
p.log.Error(err) p.log.Error(err)

View file

@ -1,3 +1,21 @@
/*
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 timeline package timeline
import ( import (

View file

@ -1,3 +1,21 @@
/*
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 timeline package timeline
import ( import (
@ -44,7 +62,7 @@ grabloop:
} }
for _, s := range filtered { for _, s := range filtered {
if _, err := t.IndexOne(s.CreatedAt, s.ID, s.BoostOfID); err != nil { if _, err := t.IndexOne(s.CreatedAt, s.ID, s.BoostOfID, s.AccountID, s.BoostOfAccountID); err != nil {
return fmt.Errorf("IndexBefore: error indexing status with id %s: %s", s.ID, err) return fmt.Errorf("IndexBefore: error indexing status with id %s: %s", s.ID, err)
} }
} }
@ -79,7 +97,7 @@ grabloop:
} }
for _, s := range filtered { for _, s := range filtered {
if _, err := t.IndexOne(s.CreatedAt, s.ID, s.BoostOfID); err != nil { if _, err := t.IndexOne(s.CreatedAt, s.ID, s.BoostOfID, s.AccountID, s.BoostOfAccountID); err != nil {
return fmt.Errorf("IndexBehind: error indexing status with id %s: %s", s.ID, err) return fmt.Errorf("IndexBehind: error indexing status with id %s: %s", s.ID, err)
} }
} }
@ -91,24 +109,29 @@ func (t *timeline) IndexOneByID(statusID string) error {
return nil return nil
} }
func (t *timeline) IndexOne(statusCreatedAt time.Time, statusID string, boostOfID string) (bool, error) { func (t *timeline) IndexOne(statusCreatedAt time.Time, statusID string, boostOfID string, accountID string, boostOfAccountID string) (bool, error) {
t.Lock() t.Lock()
defer t.Unlock() defer t.Unlock()
postIndexEntry := &postIndexEntry{ postIndexEntry := &postIndexEntry{
statusID: statusID, statusID: statusID,
boostOfID: boostOfID, boostOfID: boostOfID,
accountID: accountID,
boostOfAccountID: boostOfAccountID,
} }
return t.postIndex.insertIndexed(postIndexEntry) return t.postIndex.insertIndexed(postIndexEntry)
} }
func (t *timeline) IndexAndPrepareOne(statusCreatedAt time.Time, statusID string) (bool, error) { func (t *timeline) IndexAndPrepareOne(statusCreatedAt time.Time, statusID string, boostOfID string, accountID string, boostOfAccountID string) (bool, error) {
t.Lock() t.Lock()
defer t.Unlock() defer t.Unlock()
postIndexEntry := &postIndexEntry{ postIndexEntry := &postIndexEntry{
statusID: statusID, statusID: statusID,
boostOfID: boostOfID,
accountID: accountID,
boostOfAccountID: boostOfAccountID,
} }
inserted, err := t.postIndex.insertIndexed(postIndexEntry) inserted, err := t.postIndex.insertIndexed(postIndexEntry)

View file

@ -78,6 +78,8 @@ type Manager interface {
Remove(statusID string, timelineAccountID string) (int, error) Remove(statusID string, timelineAccountID string) (int, error)
// WipeStatusFromAllTimelines removes one status from the index and prepared posts of all timelines // WipeStatusFromAllTimelines removes one status from the index and prepared posts of all timelines
WipeStatusFromAllTimelines(statusID string) error WipeStatusFromAllTimelines(statusID string) error
// WipeStatusesFromAccountID removes all statuses by the given accountID from the timelineAccountID's timelines.
WipeStatusesFromAccountID(accountID string, timelineAccountID string) error
} }
// NewManager returns a new timeline manager with the given database, typeconverter, config, and log. // NewManager returns a new timeline manager with the given database, typeconverter, config, and log.
@ -112,7 +114,7 @@ func (m *manager) Ingest(status *gtsmodel.Status, timelineAccountID string) (boo
} }
l.Trace("ingesting status") l.Trace("ingesting status")
return t.IndexOne(status.CreatedAt, status.ID, status.BoostOfID) return t.IndexOne(status.CreatedAt, status.ID, status.BoostOfID, status.AccountID, status.BoostOfAccountID)
} }
func (m *manager) IngestAndPrepare(status *gtsmodel.Status, timelineAccountID string) (bool, error) { func (m *manager) IngestAndPrepare(status *gtsmodel.Status, timelineAccountID string) (bool, error) {
@ -128,7 +130,7 @@ func (m *manager) IngestAndPrepare(status *gtsmodel.Status, timelineAccountID st
} }
l.Trace("ingesting status") l.Trace("ingesting status")
return t.IndexAndPrepareOne(status.CreatedAt, status.ID) return t.IndexAndPrepareOne(status.CreatedAt, status.ID, status.BoostOfID, status.AccountID, status.BoostOfAccountID)
} }
func (m *manager) Remove(statusID string, timelineAccountID string) (int, error) { func (m *manager) Remove(statusID string, timelineAccountID string) (int, error) {
@ -219,6 +221,16 @@ func (m *manager) WipeStatusFromAllTimelines(statusID string) error {
return err return err
} }
func (m *manager) WipeStatusesFromAccountID(accountID string, timelineAccountID string) error {
t, err := m.getOrCreateTimeline(timelineAccountID)
if err != nil {
return err
}
_, err = t.RemoveAllBy(accountID)
return err
}
func (m *manager) getOrCreateTimeline(timelineAccountID string) (Timeline, error) { func (m *manager) getOrCreateTimeline(timelineAccountID string) (Timeline, error) {
var t Timeline var t Timeline
i, ok := m.accountTimelines.Load(timelineAccountID) i, ok := m.accountTimelines.Load(timelineAccountID)

View file

@ -1,3 +1,21 @@
/*
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 timeline package timeline
import ( import (
@ -10,8 +28,10 @@ type postIndex struct {
} }
type postIndexEntry struct { type postIndexEntry struct {
statusID string statusID string
boostOfID string boostOfID string
accountID string
boostOfAccountID string
} }
func (p *postIndex) insertIndexed(i *postIndexEntry) (bool, error) { func (p *postIndex) insertIndexed(i *postIndexEntry) (bool, error) {

View file

@ -1,3 +1,21 @@
/*
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 timeline package timeline
import ( import (
@ -207,8 +225,11 @@ func (t *timeline) prepare(statusID string) error {
// shove it in prepared posts as a prepared posts entry // shove it in prepared posts as a prepared posts entry
preparedPostsEntry := &preparedPostsEntry{ preparedPostsEntry := &preparedPostsEntry{
statusID: statusID, statusID: gtsStatus.ID,
prepared: apiModelStatus, boostOfID: gtsStatus.BoostOfID,
accountID: gtsStatus.AccountID,
boostOfAccountID: gtsStatus.BoostOfAccountID,
prepared: apiModelStatus,
} }
return t.preparedPosts.insertPrepared(preparedPostsEntry) return t.preparedPosts.insertPrepared(preparedPostsEntry)

View file

@ -1,3 +1,21 @@
/*
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 timeline package timeline
import ( import (
@ -12,8 +30,11 @@ type preparedPosts struct {
} }
type preparedPostsEntry struct { type preparedPostsEntry struct {
statusID string statusID string
prepared *apimodel.Status boostOfID string
accountID string
boostOfAccountID string
prepared *apimodel.Status
} }
func (p *preparedPosts) insertPrepared(i *preparedPostsEntry) error { func (p *preparedPosts) insertPrepared(i *preparedPostsEntry) error {

View file

@ -1,3 +1,21 @@
/*
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 timeline package timeline
import ( import (
@ -58,3 +76,55 @@ func (t *timeline) Remove(statusID string) (int, error) {
l.Debugf("removed %d entries", removed) l.Debugf("removed %d entries", removed)
return removed, nil return removed, nil
} }
func (t *timeline) RemoveAllBy(accountID string) (int, error) {
l := t.log.WithFields(logrus.Fields{
"func": "RemoveAllBy",
"accountTimeline": t.accountID,
"accountID": accountID,
})
t.Lock()
defer t.Unlock()
var removed int
// remove entr(ies) from the post index
removeIndexes := []*list.Element{}
if t.postIndex != nil && t.postIndex.data != nil {
for e := t.postIndex.data.Front(); e != nil; e = e.Next() {
entry, ok := e.Value.(*postIndexEntry)
if !ok {
return removed, errors.New("Remove: could not parse e as a postIndexEntry")
}
if entry.accountID == accountID || entry.boostOfAccountID == accountID {
l.Debug("found status in postIndex")
removeIndexes = append(removeIndexes, e)
}
}
}
for _, e := range removeIndexes {
t.postIndex.data.Remove(e)
removed = removed + 1
}
// remove entr(ies) from prepared posts
removePrepared := []*list.Element{}
if t.preparedPosts != nil && t.preparedPosts.data != nil {
for e := t.preparedPosts.data.Front(); e != nil; e = e.Next() {
entry, ok := e.Value.(*preparedPostsEntry)
if !ok {
return removed, errors.New("Remove: could not parse e as a preparedPostsEntry")
}
if entry.accountID == accountID || entry.boostOfAccountID == accountID {
l.Debug("found status in preparedPosts")
removePrepared = append(removePrepared, e)
}
}
}
for _, e := range removePrepared {
t.preparedPosts.data.Remove(e)
removed = removed + 1
}
l.Debugf("removed %d entries", removed)
return removed, nil
}

View file

@ -65,7 +65,7 @@ type Timeline interface {
// //
// The returned bool indicates whether or not the status was actually inserted into the timeline. This will be false // The returned bool indicates whether or not the status was actually inserted into the timeline. This will be false
// if the status is a boost and the original post or another boost of it already exists < boostReinsertionDepth back in the timeline. // if the status is a boost and the original post or another boost of it already exists < boostReinsertionDepth back in the timeline.
IndexOne(statusCreatedAt time.Time, statusID string, boostOfID string) (bool, error) IndexOne(statusCreatedAt time.Time, statusID string, boostOfID string, accountID string, boostOfAccountID string) (bool, error)
// OldestIndexedPostID returns the id of the rearmost (ie., the oldest) indexed post, or an error if something goes wrong. // OldestIndexedPostID returns the id of the rearmost (ie., the oldest) indexed post, or an error if something goes wrong.
// If nothing goes wrong but there's no oldest post, an empty string will be returned so make sure to check for this. // If nothing goes wrong but there's no oldest post, an empty string will be returned so make sure to check for this.
@ -85,7 +85,7 @@ type Timeline interface {
// //
// The returned bool indicates whether or not the status was actually inserted into the timeline. This will be false // The returned bool indicates whether or not the status was actually inserted into the timeline. This will be false
// if the status is a boost and the original post or another boost of it already exists < boostReinsertionDepth back in the timeline. // if the status is a boost and the original post or another boost of it already exists < boostReinsertionDepth back in the timeline.
IndexAndPrepareOne(statusCreatedAt time.Time, statusID string) (bool, error) IndexAndPrepareOne(statusCreatedAt time.Time, statusID string, boostOfID string, accountID string, boostOfAccountID string) (bool, error)
// OldestPreparedPostID returns the id of the rearmost (ie., the oldest) prepared post, or an error if something goes wrong. // OldestPreparedPostID returns the id of the rearmost (ie., the oldest) prepared post, or an error if something goes wrong.
// If nothing goes wrong but there's no oldest post, an empty string will be returned so make sure to check for this. // If nothing goes wrong but there's no oldest post, an empty string will be returned so make sure to check for this.
OldestPreparedPostID() (string, error) OldestPreparedPostID() (string, error)
@ -109,6 +109,10 @@ type Timeline interface {
// //
// The returned int indicates the amount of entries that were removed. // The returned int indicates the amount of entries that were removed.
Remove(statusID string) (int, error) Remove(statusID string) (int, error)
// RemoveAllBy removes all statuses by the given accountID, from both the index and prepared posts.
//
// The returned int indicates the amount of entries that were removed.
RemoveAllBy(accountID string) (int, error)
} }
// timeline fulfils the Timeline interface // timeline fulfils the Timeline interface

View file

@ -111,6 +111,15 @@ type Likeable interface {
withObject withObject
} }
// Blockable represents the minimum interface for an activitystreams 'block' activity.
type Blockable interface {
withJSONLDId
withTypeName
withActor
withObject
}
// Announceable represents the minimum interface for an activitystreams 'announce' activity. // Announceable represents the minimum interface for an activitystreams 'announce' activity.
type Announceable interface { type Announceable interface {
withJSONLDId withJSONLDId

View file

@ -426,6 +426,41 @@ func (c *converter) ASLikeToFave(likeable Likeable) (*gtsmodel.StatusFave, error
}, nil }, nil
} }
func (c *converter) ASBlockToBlock(blockable Blockable) (*gtsmodel.Block, error) {
idProp := blockable.GetJSONLDId()
if idProp == nil || !idProp.IsIRI() {
return nil, errors.New("ASBlockToBlock: no id property set on block, or was not an iri")
}
uri := idProp.GetIRI().String()
origin, err := extractActor(blockable)
if err != nil {
return nil, errors.New("ASBlockToBlock: error extracting actor property from block")
}
originAccount := &gtsmodel.Account{}
if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: origin.String()}}, originAccount); err != nil {
return nil, fmt.Errorf("ASBlockToBlock: error extracting account with uri %s from the database: %s", origin.String(), err)
}
target, err := extractObject(blockable)
if err != nil {
return nil, errors.New("ASBlockToBlock: error extracting object property from block")
}
targetAccount := &gtsmodel.Account{}
if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: target.String(), CaseInsensitive: true}}, targetAccount); err != nil {
return nil, fmt.Errorf("ASBlockToBlock: error extracting account with uri %s from the database: %s", target.String(), err)
}
return &gtsmodel.Block{
AccountID: originAccount.ID,
Account: originAccount,
TargetAccountID: targetAccount.ID,
TargetAccount: targetAccount,
URI: uri,
}, nil
}
func (c *converter) ASAnnounceToStatus(announceable Announceable) (*gtsmodel.Status, bool, error) { func (c *converter) ASAnnounceToStatus(announceable Announceable) (*gtsmodel.Status, bool, error) {
status := &gtsmodel.Status{} status := &gtsmodel.Status{}
isNew := true isNew := true

View file

@ -48,6 +48,10 @@ type TypeConverter interface {
// if something goes wrong. The returned account should be ready to serialize on an API level, and may NOT have sensitive fields. // if something goes wrong. The returned account should be ready to serialize on an API level, and may NOT have sensitive fields.
// In other words, this is the public record that the server has of an account. // In other words, this is the public record that the server has of an account.
AccountToMastoPublic(account *gtsmodel.Account) (*model.Account, error) AccountToMastoPublic(account *gtsmodel.Account) (*model.Account, error)
// AccountToMastoBlocked takes a db model account as a param, and returns a mastotype account, or an error if
// something goes wrong. The returned account will be a bare minimum representation of the account. This function should be used
// when someone wants to view an account they've blocked.
AccountToMastoBlocked(account *gtsmodel.Account) (*model.Account, error)
// AppToMastoSensitive takes a db model application as a param, and returns a populated mastotype application, or an error // AppToMastoSensitive takes a db model application as a param, and returns a populated mastotype application, or an error
// if something goes wrong. The returned application should be ready to serialize on an API level, and may have sensitive fields // if something goes wrong. The returned application should be ready to serialize on an API level, and may have sensitive fields
// (such as client id and client secret), so serve it only to an authorized user who should have permission to see it. // (such as client id and client secret), so serve it only to an authorized user who should have permission to see it.
@ -104,6 +108,8 @@ type TypeConverter interface {
ASFollowToFollow(followable Followable) (*gtsmodel.Follow, error) ASFollowToFollow(followable Followable) (*gtsmodel.Follow, error)
// ASLikeToFave converts a remote activitystreams 'like' representation into a gts model status fave. // ASLikeToFave converts a remote activitystreams 'like' representation into a gts model status fave.
ASLikeToFave(likeable Likeable) (*gtsmodel.StatusFave, error) ASLikeToFave(likeable Likeable) (*gtsmodel.StatusFave, error)
// ASBlockToBlock converts a remote activity streams 'block' representation into a gts model block.
ASBlockToBlock(blockable Blockable) (*gtsmodel.Block, error)
// ASAnnounceToStatus converts an activitystreams 'announce' into a status. // ASAnnounceToStatus converts an activitystreams 'announce' into a status.
// //
// The returned bool indicates whether this status is new (true) or not new (false). // The returned bool indicates whether this status is new (true) or not new (false).
@ -124,6 +130,11 @@ type TypeConverter interface {
// AccountToAS converts a gts model account into an activity streams person, suitable for federation // AccountToAS converts a gts model account into an activity streams person, suitable for federation
AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error) AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error)
// AccountToASMinimal converts a gts model account into an activity streams person, suitable for federation.
//
// The returned account will just have the Type, Username, PublicKey, and ID properties set. This is
// suitable for serving to requesters to whom we want to give as little information as possible because
// we don't trust them (yet).
AccountToASMinimal(a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error) AccountToASMinimal(a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error)
// StatusToAS converts a gts model status into an activity streams note, suitable for federation // StatusToAS converts a gts model status into an activity streams note, suitable for federation
StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, error) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, error)
@ -137,6 +148,8 @@ type TypeConverter interface {
FaveToAS(f *gtsmodel.StatusFave) (vocab.ActivityStreamsLike, error) FaveToAS(f *gtsmodel.StatusFave) (vocab.ActivityStreamsLike, error)
// BoostToAS converts a gts model boost into an activityStreams ANNOUNCE, suitable for federation // BoostToAS converts a gts model boost into an activityStreams ANNOUNCE, suitable for federation
BoostToAS(boostWrapperStatus *gtsmodel.Status, boostingAccount *gtsmodel.Account, boostedAccount *gtsmodel.Account) (vocab.ActivityStreamsAnnounce, error) BoostToAS(boostWrapperStatus *gtsmodel.Status, boostingAccount *gtsmodel.Account, boostedAccount *gtsmodel.Account) (vocab.ActivityStreamsAnnounce, error)
// BlockToAS converts a gts model block into an activityStreams BLOCK, suitable for federation.
BlockToAS(block *gtsmodel.Block) (vocab.ActivityStreamsBlock, error)
/* /*
INTERNAL (gts) MODEL TO INTERNAL MODEL INTERNAL (gts) MODEL TO INTERNAL MODEL

View file

@ -67,6 +67,7 @@ func (c *converter) StatusToBoost(s *gtsmodel.Status, boostingAccount *gtsmodel.
Language: s.Language, Language: s.Language,
Text: s.Text, Text: s.Text,
BoostOfID: s.ID, BoostOfID: s.ID,
BoostOfAccountID: s.AccountID,
Visibility: s.Visibility, Visibility: s.Visibility,
VisibilityAdvanced: s.VisibilityAdvanced, VisibilityAdvanced: s.VisibilityAdvanced,

View file

@ -780,3 +780,73 @@ func (c *converter) BoostToAS(boostWrapperStatus *gtsmodel.Status, boostingAccou
return announce, nil return announce, nil
} }
/*
we want to end up with something like this:
{
"@context": "https://www.w3.org/ns/activitystreams",
"actor": "https://example.org/users/some_user",
"id":"https://example.org/users/some_user/blocks/SOME_ULID_OF_A_BLOCK",
"object":"https://some_other.instance/users/some_other_user",
"type":"Block"
}
*/
func (c *converter) BlockToAS(b *gtsmodel.Block) (vocab.ActivityStreamsBlock, error) {
if b.Account == nil {
a := &gtsmodel.Account{}
if err := c.db.GetByID(b.AccountID, a); err != nil {
return nil, fmt.Errorf("BlockToAS: error getting block account from database: %s", err)
}
b.Account = a
}
if b.TargetAccount == nil {
a := &gtsmodel.Account{}
if err := c.db.GetByID(b.TargetAccountID, a); err != nil {
return nil, fmt.Errorf("BlockToAS: error getting block target account from database: %s", err)
}
b.TargetAccount = a
}
// create the block
block := streams.NewActivityStreamsBlock()
// set the actor property to the block-ing account's URI
actorProp := streams.NewActivityStreamsActorProperty()
actorIRI, err := url.Parse(b.Account.URI)
if err != nil {
return nil, fmt.Errorf("BlockToAS: error parsing uri %s: %s", b.Account.URI, err)
}
actorProp.AppendIRI(actorIRI)
block.SetActivityStreamsActor(actorProp)
// set the ID property to the blocks's URI
idProp := streams.NewJSONLDIdProperty()
idIRI, err := url.Parse(b.URI)
if err != nil {
return nil, fmt.Errorf("BlockToAS: error parsing uri %s: %s", b.URI, err)
}
idProp.Set(idIRI)
block.SetJSONLDId(idProp)
// set the object property to the target account's URI
objectProp := streams.NewActivityStreamsObjectProperty()
targetIRI, err := url.Parse(b.TargetAccount.URI)
if err != nil {
return nil, fmt.Errorf("BlockToAS: error parsing uri %s: %s", b.TargetAccount.URI, err)
}
objectProp.AppendIRI(targetIRI)
block.SetActivityStreamsObject(objectProp)
// set the TO property to the target account's IRI
toProp := streams.NewActivityStreamsToProperty()
toIRI, err := url.Parse(b.TargetAccount.URI)
if err != nil {
return nil, fmt.Errorf("BlockToAS: error parsing uri %s: %s", b.TargetAccount.URI, err)
}
toProp.AppendIRI(toIRI)
block.SetActivityStreamsTo(toProp)
return block, nil
}

View file

@ -150,6 +150,11 @@ func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*model.Account, e
acct = a.Username acct = a.Username
} }
var suspended bool
if !a.SuspendedAt.IsZero() {
suspended = true
}
return &model.Account{ return &model.Account{
ID: a.ID, ID: a.ID,
Username: a.Username, Username: a.Username,
@ -170,6 +175,34 @@ func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*model.Account, e
LastStatusAt: lastStatusAt, LastStatusAt: lastStatusAt,
Emojis: emojis, // TODO: implement this Emojis: emojis, // TODO: implement this
Fields: fields, Fields: fields,
Suspended: suspended,
}, nil
}
func (c *converter) AccountToMastoBlocked(a *gtsmodel.Account) (*model.Account, error) {
var acct string
if a.Domain != "" {
// this is a remote user
acct = fmt.Sprintf("%s@%s", a.Username, a.Domain)
} else {
// this is a local user
acct = a.Username
}
var suspended bool
if !a.SuspendedAt.IsZero() {
suspended = true
}
return &model.Account{
ID: a.ID,
Username: a.Username,
Acct: acct,
DisplayName: a.DisplayName,
Bot: a.Bot,
CreatedAt: a.CreatedAt.Format(time.RFC3339),
URL: a.URL,
Suspended: suspended,
}, nil }, nil
} }

View file

@ -104,4 +104,9 @@ var (
// from eg /users/example_username/statuses/01F7XT5JZW1WMVSW1KADS8PVDH // from eg /users/example_username/statuses/01F7XT5JZW1WMVSW1KADS8PVDH
// The regex can be played with here: https://regex101.com/r/G9zuxQ/1 // The regex can be played with here: https://regex101.com/r/G9zuxQ/1
statusesPathRegex = regexp.MustCompile(statusesPathRegexString) statusesPathRegex = regexp.MustCompile(statusesPathRegexString)
blockPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, UsersPath, usernameRegexString, BlocksPath, ulidRegexString)
// blockPathRegex parses a path that validates and captures the username part and the ulid part
// from eg /users/example_username/blocks/01F7XT5JZW1WMVSW1KADS8PVDH
blockPathRegex = regexp.MustCompile(blockPathRegexString)
) )

View file

@ -50,6 +50,8 @@ const (
FollowPath = "follow" FollowPath = "follow"
// UpdatePath is used to generate the URI for an account update // UpdatePath is used to generate the URI for an account update
UpdatePath = "updates" UpdatePath = "updates"
// BlocksPath is used to generate the URI for a block
BlocksPath = "blocks"
) )
// APContextKey is a type used specifically for settings values on contexts within go-fed AP request chains // APContextKey is a type used specifically for settings values on contexts within go-fed AP request chains
@ -124,6 +126,12 @@ func GenerateURIForUpdate(username string, protocol string, host string, thisUpd
return fmt.Sprintf("%s://%s/%s/%s#%s/%s", protocol, host, UsersPath, username, UpdatePath, thisUpdateID) return fmt.Sprintf("%s://%s/%s/%s#%s/%s", protocol, host, UsersPath, username, UpdatePath, thisUpdateID)
} }
// GenerateURIForBlock returns the AP URI for a new block activity -- something like:
// https://example.org/users/whatever_user/blocks/01F7XTH1QGBAPMGF49WJZ91XGC
func GenerateURIForBlock(username string, protocol string, host string, thisBlockID string) string {
return fmt.Sprintf("%s://%s/%s/%s/%s/%s", protocol, host, UsersPath, username, BlocksPath, thisBlockID)
}
// GenerateURIsForAccount throws together a bunch of URIs for the given username, with the given protocol and host. // GenerateURIsForAccount throws together a bunch of URIs for the given username, with the given protocol and host.
func GenerateURIsForAccount(username string, protocol string, host string) *UserURIs { func GenerateURIsForAccount(username string, protocol string, host string) *UserURIs {
// The below URLs are used for serving web requests // The below URLs are used for serving web requests
@ -214,6 +222,11 @@ func IsPublicKeyPath(id *url.URL) bool {
return userPublicKeyPathRegex.MatchString(id.Path) return userPublicKeyPathRegex.MatchString(id.Path)
} }
// IsBlockPath returns true if the given URL path corresponds to eg /users/example_username/blocks/SOME_ULID_OF_A_BLOCK
func IsBlockPath(id *url.URL) bool {
return blockPathRegex.MatchString(id.Path)
}
// ParseStatusesPath returns the username and ulid from a path such as /users/example_username/statuses/SOME_ULID_OF_A_STATUS // ParseStatusesPath returns the username and ulid from a path such as /users/example_username/statuses/SOME_ULID_OF_A_STATUS
func ParseStatusesPath(id *url.URL) (username string, ulid string, err error) { func ParseStatusesPath(id *url.URL) (username string, ulid string, err error) {
matches := statusesPathRegex.FindStringSubmatch(id.Path) matches := statusesPathRegex.FindStringSubmatch(id.Path)
@ -292,3 +305,15 @@ func ParseLikedPath(id *url.URL) (username string, ulid string, err error) {
ulid = matches[2] ulid = matches[2]
return return
} }
// ParseBlockPath returns the username and ulid from a path such as /users/example_username/blocks/SOME_ULID_OF_A_BLOCK
func ParseBlockPath(id *url.URL) (username string, ulid string, err error) {
matches := blockPathRegex.FindStringSubmatch(id.Path)
if len(matches) != 3 {
err = fmt.Errorf("expected 3 matches but matches length was %d", len(matches))
return
}
username = matches[1]
ulid = matches[2]
return
}