forked from mirrors/gotosocial
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:
parent
c7da64922f
commit
846057f0d6
45 changed files with 1405 additions and 63 deletions
|
@ -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)
|
||||
* [x] /api/v1/accounts/:id/follow POST (Follow this account)
|
||||
* [x] /api/v1/accounts/:id/unfollow POST (Unfollow this account)
|
||||
* [ ] /api/v1/accounts/:id/block POST (Block this account)
|
||||
* [ ] /api/v1/accounts/:id/unblock POST (Unblock this account)
|
||||
* [x] /api/v1/accounts/:id/block POST (Block 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/unmute POST (Unmute this account)
|
||||
* [ ] /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)
|
||||
* [ ] Mutes
|
||||
* [ ] /api/v1/mutes GET (See list of muted accounts)
|
||||
* [ ] Blocks
|
||||
* [ ] /api/v1/blocks GET (See list of blocked accounts)
|
||||
* [x] Blocks
|
||||
* [x] /api/v1/blocks GET (See list of blocked accounts)
|
||||
* [ ] Domain Blocks
|
||||
* [x] /api/v1/domain_blocks GET (See list of domain blocks)
|
||||
* [x] /api/v1/domain_blocks POST (Create a domain block)
|
||||
|
|
|
@ -61,10 +61,14 @@ const (
|
|||
GetFollowingPath = BasePathWithID + "/following"
|
||||
// GetRelationshipsPath is for showing an account's relationship with other accounts
|
||||
GetRelationshipsPath = BasePath + "/relationships"
|
||||
// PostFollowPath is for POSTing new follows to, and updating existing follows
|
||||
PostFollowPath = BasePathWithID + "/follow"
|
||||
// PostUnfollowPath is for POSTing an unfollow
|
||||
PostUnfollowPath = BasePathWithID + "/unfollow"
|
||||
// FollowPath is for POSTing new follows to, and updating existing follows
|
||||
FollowPath = BasePathWithID + "/follow"
|
||||
// UnfollowPath is for POSTing an 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
|
||||
|
@ -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
|
||||
func (m *Module) Route(r router.Router) error {
|
||||
// create account
|
||||
r.AttachHandler(http.MethodPost, BasePath, m.AccountCreatePOSTHandler)
|
||||
|
||||
// get account
|
||||
r.AttachHandler(http.MethodGet, BasePathWithID, m.muxHandler)
|
||||
|
||||
// modify account
|
||||
r.AttachHandler(http.MethodPatch, BasePathWithID, m.muxHandler)
|
||||
|
||||
// get account's statuses
|
||||
r.AttachHandler(http.MethodGet, GetStatusesPath, m.AccountStatusesGETHandler)
|
||||
|
||||
// get following or followers
|
||||
r.AttachHandler(http.MethodGet, GetFollowersPath, m.AccountFollowersGETHandler)
|
||||
r.AttachHandler(http.MethodGet, GetFollowingPath, m.AccountFollowingGETHandler)
|
||||
|
||||
// get relationship with account
|
||||
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
|
||||
}
|
||||
|
||||
|
|
49
internal/api/client/account/block.go
Normal file
49
internal/api/client/account/block.go
Normal 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)
|
||||
}
|
49
internal/api/client/account/unblock.go
Normal file
49
internal/api/client/account/unblock.go
Normal 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)
|
||||
}
|
63
internal/api/client/blocks/blocks.go
Normal file
63
internal/api/client/blocks/blocks.go
Normal 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
|
||||
}
|
75
internal/api/client/blocks/blocksget.go
Normal file
75
internal/api/client/blocks/blocksget.go
Normal 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)
|
||||
}
|
|
@ -67,23 +67,23 @@ sendLoop:
|
|||
select {
|
||||
case m := <-stream.Messages:
|
||||
// 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 {
|
||||
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
|
||||
break sendLoop
|
||||
}
|
||||
l.Debug("wrote message into websocket connection")
|
||||
l.Trace("wrote message into websocket connection")
|
||||
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 {
|
||||
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
|
||||
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")
|
||||
}
|
||||
|
|
26
internal/api/model/block.go
Normal file
26
internal/api/model/block.go
Normal 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
|
||||
}
|
|
@ -14,6 +14,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/api/client/admin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/app"
|
||||
"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/favourites"
|
||||
"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)
|
||||
streamingModule := streaming.New(c, processor, log)
|
||||
favouritesModule := favourites.New(c, processor, log)
|
||||
blocksModule := blocks.New(c, processor, log)
|
||||
|
||||
apis := []api.ClientModule{
|
||||
// modules with middleware go first
|
||||
|
@ -170,6 +172,7 @@ var Start cliactions.GTSAction = func(ctx context.Context, c *config.Config, log
|
|||
listsModule,
|
||||
streamingModule,
|
||||
favouritesModule,
|
||||
blocksModule,
|
||||
}
|
||||
|
||||
for _, m := range apis {
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/api/client/admin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/app"
|
||||
"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/favourites"
|
||||
"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)
|
||||
streamingModule := streaming.New(c, processor, log)
|
||||
favouritesModule := favourites.New(c, processor, log)
|
||||
blocksModule := blocks.New(c, processor, log)
|
||||
|
||||
apis := []api.ClientModule{
|
||||
// modules with middleware go first
|
||||
|
@ -115,6 +117,7 @@ var Start cliactions.GTSAction = func(ctx context.Context, _ *config.Config, log
|
|||
listsModule,
|
||||
streamingModule,
|
||||
favouritesModule,
|
||||
blocksModule,
|
||||
}
|
||||
|
||||
for _, m := range apis {
|
||||
|
|
|
@ -159,6 +159,8 @@ type DB interface {
|
|||
// 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)
|
||||
|
||||
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.
|
||||
// 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
|
||||
|
|
67
internal/db/pg/blocks.go
Normal file
67
internal/db/pg/blocks.go
Normal 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
|
||||
}
|
|
@ -393,6 +393,7 @@ func (f *federator) DereferenceAnnounce(announce *gtsmodel.Status, requestingUse
|
|||
announce.Language = boostedStatus.Language
|
||||
announce.Text = boostedStatus.Text
|
||||
announce.BoostOfID = boostedStatus.ID
|
||||
announce.BoostOfAccountID = boostedStatus.AccountID
|
||||
announce.Visibility = boostedStatus.Visibility
|
||||
announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced
|
||||
announce.GTSBoostedStatus = boostedStatus
|
||||
|
@ -477,6 +478,7 @@ func (f *federator) DereferenceAnnounce(announce *gtsmodel.Status, requestingUse
|
|||
announce.Language = boostedStatus.Language
|
||||
announce.Text = boostedStatus.Text
|
||||
announce.BoostOfID = boostedStatus.ID
|
||||
announce.BoostOfAccountID = boostedStatus.AccountID
|
||||
announce.Visibility = boostedStatus.Visibility
|
||||
announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced
|
||||
announce.GTSBoostedStatus = boostedStatus
|
||||
|
|
|
@ -129,6 +129,7 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
|
|||
}
|
||||
}
|
||||
case gtsmodel.ActivityStreamsFollow:
|
||||
// FOLLOW SOMETHING
|
||||
follow, ok := asType.(vocab.ActivityStreamsFollow)
|
||||
if !ok {
|
||||
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,
|
||||
}
|
||||
case gtsmodel.ActivityStreamsLike:
|
||||
// LIKE SOMETHING
|
||||
like, ok := asType.(vocab.ActivityStreamsLike)
|
||||
if !ok {
|
||||
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,
|
||||
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
|
||||
}
|
||||
|
|
|
@ -39,16 +39,15 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {
|
|||
"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 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
|
||||
}
|
||||
|
||||
// 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
|
||||
if util.IsStatusesPath(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
|
||||
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
|
||||
}
|
||||
|
||||
// check if it's a user, eg /users/example_username
|
||||
if util.IsUserPath(id) {
|
||||
username, err := util.ParseUserPath(id)
|
||||
if err != nil {
|
||||
|
@ -81,7 +79,7 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {
|
|||
// an actual error happened
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -98,7 +96,7 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {
|
|||
// an actual error happened
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -115,7 +113,57 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {
|
|||
// an actual error happened
|
||||
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, >smodel.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, >smodel.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, >smodel.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, >smodel.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
|
||||
}
|
||||
|
||||
|
|
|
@ -85,6 +85,31 @@ func (f *federatingDB) Undo(ctx context.Context, undo vocab.ActivityStreamsUndo)
|
|||
// UNDO LIKE
|
||||
case string(gtsmodel.ActivityStreamsAnnounce):
|
||||
// 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}}, >smodel.Block{}); err != nil {
|
||||
return fmt.Errorf("UNDO: db error removing block: %s", err)
|
||||
}
|
||||
l.Debug("block undone")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
announce, ok := t.(vocab.ActivityStreamsAnnounce)
|
||||
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()
|
||||
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
|
||||
update, ok := t.(vocab.ActivityStreamsUpdate)
|
||||
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()
|
||||
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
|
||||
}
|
||||
}
|
||||
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
|
||||
|
|
|
@ -243,8 +243,8 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er
|
|||
return true, nil
|
||||
}
|
||||
|
||||
a := >smodel.Account{}
|
||||
if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, a); err != nil {
|
||||
requestingAccount := >smodel.Account{}
|
||||
if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, requestingAccount); err != nil {
|
||||
_, ok := err.(db.ErrNoEntries)
|
||||
if ok {
|
||||
// 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)
|
||||
}
|
||||
blocked, err := f.db.Blocked(requestedAccount.ID, a.ID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error checking account blocks: %s", err)
|
||||
}
|
||||
if blocked {
|
||||
|
||||
// check if requested account blocks requesting account
|
||||
if err := f.db.GetWhere([]db.Where{
|
||||
{Key: "account_id", Value: requestedAccount.ID},
|
||||
{Key: "target_account_id", Value: requestingAccount.ID},
|
||||
}, >smodel.Block{}); err == nil {
|
||||
// a block exists
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,9 +11,11 @@ type Block struct {
|
|||
// When was this block updated
|
||||
UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
||||
// 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?
|
||||
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
|
||||
URI string
|
||||
URI string `pg:",notnull"`
|
||||
}
|
||||
|
|
|
@ -56,6 +56,8 @@ type Status struct {
|
|||
InReplyToAccountID string `pg:"type:CHAR(26)"`
|
||||
// id of the status this status is a boost of
|
||||
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
|
||||
ContentWarning string
|
||||
// visibility entry for this status
|
||||
|
|
|
@ -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) {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -59,6 +59,11 @@ type Processor interface {
|
|||
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(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,
|
||||
// parsing and checking the image, and doing the necessary updates in the database for this to become
|
||||
// the account's new header image.
|
||||
|
|
155
internal/processing/account/createblock.go
Normal file
155
internal/processing/account/createblock.go
Normal 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 := >smodel.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 := >smodel.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},
|
||||
}, >smodel.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},
|
||||
}, >smodel.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 := >smodel.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 := >smodel.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: >smodel.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: >smodel.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)
|
||||
}
|
|
@ -45,9 +45,19 @@ func (p *processor) Get(requestingAccount *gtsmodel.Account, targetAccountID str
|
|||
p.log.WithField("func", "AccountGet").Debugf("dereferencing account: %s", err)
|
||||
}
|
||||
|
||||
var mastoAccount *apimodel.Account
|
||||
var blocked bool
|
||||
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)
|
||||
} else {
|
||||
mastoAccount, err = p.tc.AccountToMastoPublic(targetAccount)
|
||||
|
|
67
internal/processing/account/removeblock.go
Normal file
67
internal/processing/account/removeblock.go
Normal 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 := >smodel.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 := >smodel.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, >smodel.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)
|
||||
}
|
83
internal/processing/blocks.go
Normal file
83
internal/processing/blocks.go
Normal 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
|
||||
}
|
|
@ -76,7 +76,6 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error
|
|||
}
|
||||
|
||||
return p.federateFave(fave, clientMsg.OriginAccount, clientMsg.TargetAccount)
|
||||
|
||||
case gtsmodel.ActivityStreamsAnnounce:
|
||||
// CREATE BOOST/ANNOUNCE
|
||||
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)
|
||||
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:
|
||||
// UPDATE
|
||||
|
@ -132,6 +150,13 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error
|
|||
return errors.New("undo was not parseable as *gtsmodel.Follow")
|
||||
}
|
||||
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:
|
||||
// UNDO LIKE/FAVE
|
||||
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)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *processor) federateBlock(block *gtsmodel.Block) error {
|
||||
if block.Account == nil {
|
||||
a := >smodel.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 := >smodel.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 := >smodel.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 := >smodel.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
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
|
|||
"federatorMsg": fmt.Sprintf("%+v", federatorMsg),
|
||||
})
|
||||
|
||||
l.Debug("entering function PROCESS FROM FEDERATOR")
|
||||
l.Trace("entering function PROCESS FROM FEDERATOR")
|
||||
|
||||
switch federatorMsg.APActivityType {
|
||||
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")
|
||||
}
|
||||
|
||||
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 {
|
||||
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")
|
||||
}
|
||||
|
||||
l.Debug("will now derefence incoming account")
|
||||
l.Trace("will now derefence incoming account")
|
||||
if err := p.federator.DereferenceAccountFields(incomingAccount, "", false); err != nil {
|
||||
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 {
|
||||
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:
|
||||
// UPDATE
|
||||
|
@ -138,7 +154,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
|
|||
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 {
|
||||
return fmt.Errorf("error dereferencing account from federator: %s", err)
|
||||
}
|
||||
|
|
|
@ -82,6 +82,10 @@ type Processor interface {
|
|||
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(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(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(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(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error)
|
||||
|
||||
|
@ -275,14 +282,14 @@ func (p *processor) Start() error {
|
|||
for {
|
||||
select {
|
||||
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() {
|
||||
if err := p.processFromClientAPI(clientMsg); err != nil {
|
||||
p.log.Error(err)
|
||||
}
|
||||
}()
|
||||
case federatorMsg := <-p.fromFederator:
|
||||
p.log.Infof("received message FROM federator: %+v", federatorMsg)
|
||||
p.log.Tracef("received message FROM federator: %+v", federatorMsg)
|
||||
go func() {
|
||||
if err := p.processFromFederator(federatorMsg); err != nil {
|
||||
p.log.Error(err)
|
||||
|
|
|
@ -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
|
||||
|
||||
import (
|
||||
|
|
|
@ -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
|
||||
|
||||
import (
|
||||
|
@ -44,7 +62,7 @@ grabloop:
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -79,7 +97,7 @@ grabloop:
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -91,24 +109,29 @@ func (t *timeline) IndexOneByID(statusID string) error {
|
|||
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()
|
||||
defer t.Unlock()
|
||||
|
||||
postIndexEntry := &postIndexEntry{
|
||||
statusID: statusID,
|
||||
boostOfID: boostOfID,
|
||||
statusID: statusID,
|
||||
boostOfID: boostOfID,
|
||||
accountID: accountID,
|
||||
boostOfAccountID: boostOfAccountID,
|
||||
}
|
||||
|
||||
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()
|
||||
defer t.Unlock()
|
||||
|
||||
postIndexEntry := &postIndexEntry{
|
||||
statusID: statusID,
|
||||
statusID: statusID,
|
||||
boostOfID: boostOfID,
|
||||
accountID: accountID,
|
||||
boostOfAccountID: boostOfAccountID,
|
||||
}
|
||||
|
||||
inserted, err := t.postIndex.insertIndexed(postIndexEntry)
|
||||
|
|
|
@ -78,6 +78,8 @@ type Manager interface {
|
|||
Remove(statusID string, timelineAccountID string) (int, error)
|
||||
// WipeStatusFromAllTimelines removes one status from the index and prepared posts of all timelines
|
||||
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.
|
||||
|
@ -112,7 +114,7 @@ func (m *manager) Ingest(status *gtsmodel.Status, timelineAccountID string) (boo
|
|||
}
|
||||
|
||||
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) {
|
||||
|
@ -128,7 +130,7 @@ func (m *manager) IngestAndPrepare(status *gtsmodel.Status, timelineAccountID st
|
|||
}
|
||||
|
||||
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) {
|
||||
|
@ -219,6 +221,16 @@ func (m *manager) WipeStatusFromAllTimelines(statusID string) error {
|
|||
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) {
|
||||
var t Timeline
|
||||
i, ok := m.accountTimelines.Load(timelineAccountID)
|
||||
|
|
|
@ -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
|
||||
|
||||
import (
|
||||
|
@ -10,8 +28,10 @@ type postIndex struct {
|
|||
}
|
||||
|
||||
type postIndexEntry struct {
|
||||
statusID string
|
||||
boostOfID string
|
||||
statusID string
|
||||
boostOfID string
|
||||
accountID string
|
||||
boostOfAccountID string
|
||||
}
|
||||
|
||||
func (p *postIndex) insertIndexed(i *postIndexEntry) (bool, error) {
|
||||
|
|
|
@ -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
|
||||
|
||||
import (
|
||||
|
@ -207,8 +225,11 @@ func (t *timeline) prepare(statusID string) error {
|
|||
|
||||
// shove it in prepared posts as a prepared posts entry
|
||||
preparedPostsEntry := &preparedPostsEntry{
|
||||
statusID: statusID,
|
||||
prepared: apiModelStatus,
|
||||
statusID: gtsStatus.ID,
|
||||
boostOfID: gtsStatus.BoostOfID,
|
||||
accountID: gtsStatus.AccountID,
|
||||
boostOfAccountID: gtsStatus.BoostOfAccountID,
|
||||
prepared: apiModelStatus,
|
||||
}
|
||||
|
||||
return t.preparedPosts.insertPrepared(preparedPostsEntry)
|
||||
|
|
|
@ -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
|
||||
|
||||
import (
|
||||
|
@ -12,8 +30,11 @@ type preparedPosts struct {
|
|||
}
|
||||
|
||||
type preparedPostsEntry struct {
|
||||
statusID string
|
||||
prepared *apimodel.Status
|
||||
statusID string
|
||||
boostOfID string
|
||||
accountID string
|
||||
boostOfAccountID string
|
||||
prepared *apimodel.Status
|
||||
}
|
||||
|
||||
func (p *preparedPosts) insertPrepared(i *preparedPostsEntry) error {
|
||||
|
|
|
@ -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
|
||||
|
||||
import (
|
||||
|
@ -58,3 +76,55 @@ func (t *timeline) Remove(statusID string) (int, error) {
|
|||
l.Debugf("removed %d entries", removed)
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
// 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.
|
||||
// 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
|
||||
// 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.
|
||||
// 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)
|
||||
|
@ -109,6 +109,10 @@ type Timeline interface {
|
|||
//
|
||||
// The returned int indicates the amount of entries that were removed.
|
||||
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
|
||||
|
|
|
@ -111,6 +111,15 @@ type Likeable interface {
|
|||
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.
|
||||
type Announceable interface {
|
||||
withJSONLDId
|
||||
|
|
|
@ -426,6 +426,41 @@ func (c *converter) ASLikeToFave(likeable Likeable) (*gtsmodel.StatusFave, error
|
|||
}, 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 := >smodel.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 := >smodel.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 >smodel.Block{
|
||||
AccountID: originAccount.ID,
|
||||
Account: originAccount,
|
||||
TargetAccountID: targetAccount.ID,
|
||||
TargetAccount: targetAccount,
|
||||
URI: uri,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *converter) ASAnnounceToStatus(announceable Announceable) (*gtsmodel.Status, bool, error) {
|
||||
status := >smodel.Status{}
|
||||
isNew := true
|
||||
|
|
|
@ -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.
|
||||
// In other words, this is the public record that the server has of an account.
|
||||
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
|
||||
// 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.
|
||||
|
@ -104,6 +108,8 @@ type TypeConverter interface {
|
|||
ASFollowToFollow(followable Followable) (*gtsmodel.Follow, error)
|
||||
// ASLikeToFave converts a remote activitystreams 'like' representation into a gts model status fave.
|
||||
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.
|
||||
//
|
||||
// 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(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)
|
||||
// StatusToAS converts a gts model status into an activity streams note, suitable for federation
|
||||
StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, error)
|
||||
|
@ -137,6 +148,8 @@ type TypeConverter interface {
|
|||
FaveToAS(f *gtsmodel.StatusFave) (vocab.ActivityStreamsLike, error)
|
||||
// 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)
|
||||
// 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
|
||||
|
|
|
@ -67,6 +67,7 @@ func (c *converter) StatusToBoost(s *gtsmodel.Status, boostingAccount *gtsmodel.
|
|||
Language: s.Language,
|
||||
Text: s.Text,
|
||||
BoostOfID: s.ID,
|
||||
BoostOfAccountID: s.AccountID,
|
||||
Visibility: s.Visibility,
|
||||
VisibilityAdvanced: s.VisibilityAdvanced,
|
||||
|
||||
|
|
|
@ -780,3 +780,73 @@ func (c *converter) BoostToAS(boostWrapperStatus *gtsmodel.Status, boostingAccou
|
|||
|
||||
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 := >smodel.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 := >smodel.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
|
||||
}
|
||||
|
|
|
@ -150,6 +150,11 @@ func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*model.Account, e
|
|||
acct = a.Username
|
||||
}
|
||||
|
||||
var suspended bool
|
||||
if !a.SuspendedAt.IsZero() {
|
||||
suspended = true
|
||||
}
|
||||
|
||||
return &model.Account{
|
||||
ID: a.ID,
|
||||
Username: a.Username,
|
||||
|
@ -170,6 +175,34 @@ func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*model.Account, e
|
|||
LastStatusAt: lastStatusAt,
|
||||
Emojis: emojis, // TODO: implement this
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -104,4 +104,9 @@ var (
|
|||
// from eg /users/example_username/statuses/01F7XT5JZW1WMVSW1KADS8PVDH
|
||||
// The regex can be played with here: https://regex101.com/r/G9zuxQ/1
|
||||
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)
|
||||
)
|
||||
|
|
|
@ -50,6 +50,8 @@ const (
|
|||
FollowPath = "follow"
|
||||
// UpdatePath is used to generate the URI for an account update
|
||||
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
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func GenerateURIsForAccount(username string, protocol string, host string) *UserURIs {
|
||||
// The below URLs are used for serving web requests
|
||||
|
@ -214,6 +222,11 @@ func IsPublicKeyPath(id *url.URL) bool {
|
|||
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
|
||||
func ParseStatusesPath(id *url.URL) (username string, ulid string, err error) {
|
||||
matches := statusesPathRegex.FindStringSubmatch(id.Path)
|
||||
|
@ -292,3 +305,15 @@ func ParseLikedPath(id *url.URL) (username string, ulid string, err error) {
|
|||
ulid = matches[2]
|
||||
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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue