From 846057f0d696fded87d105dec1245e9ba32763ce Mon Sep 17 00:00:00 2001
From: Tobi Smethurst <31960611+tsmethurst@users.noreply.github.com>
Date: Sun, 11 Jul 2021 16:22:21 +0200
Subject: [PATCH] Block/unblock (#96)
* remote + local block logic, incl. federation
* improve blocking stuff
* fiddle with display of blocked profiles
* go fmt
---
PROGRESS.md | 8 +-
internal/api/client/account/account.go | 34 ++++-
internal/api/client/account/block.go | 49 +++++++
internal/api/client/account/unblock.go | 49 +++++++
internal/api/client/blocks/blocks.go | 63 +++++++++
internal/api/client/blocks/blocksget.go | 75 ++++++++++
internal/api/client/streaming/stream.go | 14 +-
internal/api/model/block.go | 26 ++++
internal/cliactions/server/server.go | 3 +
internal/cliactions/testrig/testrig.go | 3 +
internal/db/db.go | 2 +
internal/db/pg/blocks.go | 67 +++++++++
internal/federation/dereference.go | 2 +
internal/federation/federatingdb/create.go | 30 ++++
internal/federation/federatingdb/owns.go | 64 +++++++--
internal/federation/federatingdb/undo.go | 25 ++++
internal/federation/federatingdb/util.go | 30 +++-
internal/federation/federatingprotocol.go | 16 ++-
internal/gtsmodel/block.go | 8 +-
internal/gtsmodel/status.go | 2 +
internal/processing/account.go | 8 ++
internal/processing/account/account.go | 5 +
internal/processing/account/createblock.go | 155 +++++++++++++++++++++
internal/processing/account/get.go | 14 +-
internal/processing/account/removeblock.go | 67 +++++++++
internal/processing/blocks.go | 83 +++++++++++
internal/processing/fromclientapi.go | 117 +++++++++++++++-
internal/processing/fromfederator.go | 24 +++-
internal/processing/processor.go | 11 +-
internal/timeline/get.go | 18 +++
internal/timeline/index.go | 37 ++++-
internal/timeline/manager.go | 16 ++-
internal/timeline/postindex.go | 24 +++-
internal/timeline/prepare.go | 25 +++-
internal/timeline/preparedposts.go | 25 +++-
internal/timeline/remove.go | 70 ++++++++++
internal/timeline/timeline.go | 8 +-
internal/typeutils/asinterfaces.go | 9 ++
internal/typeutils/astointernal.go | 35 +++++
internal/typeutils/converter.go | 13 ++
internal/typeutils/internal.go | 1 +
internal/typeutils/internaltoas.go | 70 ++++++++++
internal/typeutils/internaltofrontend.go | 33 +++++
internal/util/regexes.go | 5 +
internal/util/uri.go | 25 ++++
45 files changed, 1405 insertions(+), 63 deletions(-)
create mode 100644 internal/api/client/account/block.go
create mode 100644 internal/api/client/account/unblock.go
create mode 100644 internal/api/client/blocks/blocks.go
create mode 100644 internal/api/client/blocks/blocksget.go
create mode 100644 internal/api/model/block.go
create mode 100644 internal/db/pg/blocks.go
create mode 100644 internal/processing/account/createblock.go
create mode 100644 internal/processing/account/removeblock.go
create mode 100644 internal/processing/blocks.go
diff --git a/PROGRESS.md b/PROGRESS.md
index 54e11d2b5..1d824c42b 100644
--- a/PROGRESS.md
+++ b/PROGRESS.md
@@ -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)
diff --git a/internal/api/client/account/account.go b/internal/api/client/account/account.go
index 3a820c0ea..42aca3283 100644
--- a/internal/api/client/account/account.go
+++ b/internal/api/client/account/account.go
@@ -61,10 +61,14 @@
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
}
diff --git a/internal/api/client/account/block.go b/internal/api/client/account/block.go
new file mode 100644
index 000000000..c83837c2a
--- /dev/null
+++ b/internal/api/client/account/block.go
@@ -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 .
+*/
+
+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)
+}
diff --git a/internal/api/client/account/unblock.go b/internal/api/client/account/unblock.go
new file mode 100644
index 000000000..1cb959db9
--- /dev/null
+++ b/internal/api/client/account/unblock.go
@@ -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 .
+*/
+
+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)
+}
diff --git a/internal/api/client/blocks/blocks.go b/internal/api/client/blocks/blocks.go
new file mode 100644
index 000000000..de87e892f
--- /dev/null
+++ b/internal/api/client/blocks/blocks.go
@@ -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 .
+*/
+
+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
+}
diff --git a/internal/api/client/blocks/blocksget.go b/internal/api/client/blocks/blocksget.go
new file mode 100644
index 000000000..bf5f41e40
--- /dev/null
+++ b/internal/api/client/blocks/blocksget.go
@@ -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 .
+*/
+
+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)
+}
diff --git a/internal/api/client/streaming/stream.go b/internal/api/client/streaming/stream.go
index a5b8a2d99..20a12fefe 100644
--- a/internal/api/client/streaming/stream.go
+++ b/internal/api/client/streaming/stream.go
@@ -67,23 +67,23 @@ func (m *Module) StreamGETHandler(c *gin.Context) {
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")
}
diff --git a/internal/api/model/block.go b/internal/api/model/block.go
new file mode 100644
index 000000000..6d6b15833
--- /dev/null
+++ b/internal/api/model/block.go
@@ -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 .
+*/
+
+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
+}
diff --git a/internal/cliactions/server/server.go b/internal/cliactions/server/server.go
index b8eb2e381..316be614e 100644
--- a/internal/cliactions/server/server.go
+++ b/internal/cliactions/server/server.go
@@ -14,6 +14,7 @@
"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 @@
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 @@
listsModule,
streamingModule,
favouritesModule,
+ blocksModule,
}
for _, m := range apis {
diff --git a/internal/cliactions/testrig/testrig.go b/internal/cliactions/testrig/testrig.go
index 312d19a62..c669dd851 100644
--- a/internal/cliactions/testrig/testrig.go
+++ b/internal/cliactions/testrig/testrig.go
@@ -16,6 +16,7 @@
"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 @@
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 @@
listsModule,
streamingModule,
favouritesModule,
+ blocksModule,
}
for _, m := range apis {
diff --git a/internal/db/db.go b/internal/db/db.go
index 0a3979df6..bbe780e80 100644
--- a/internal/db/db.go
+++ b/internal/db/db.go
@@ -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
diff --git a/internal/db/pg/blocks.go b/internal/db/pg/blocks.go
new file mode 100644
index 000000000..a6fc1f859
--- /dev/null
+++ b/internal/db/pg/blocks.go
@@ -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 .
+*/
+
+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
+}
diff --git a/internal/federation/dereference.go b/internal/federation/dereference.go
index 20ffa3a8d..b87462acd 100644
--- a/internal/federation/dereference.go
+++ b/internal/federation/dereference.go
@@ -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
diff --git a/internal/federation/federatingdb/create.go b/internal/federation/federatingdb/create.go
index 02a5dfd72..3562e7f4c 100644
--- a/internal/federation/federatingdb/create.go
+++ b/internal/federation/federatingdb/create.go
@@ -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
}
diff --git a/internal/federation/federatingdb/owns.go b/internal/federation/federatingdb/owns.go
index fe7160021..51b20151a 100644
--- a/internal/federation/federatingdb/owns.go
+++ b/internal/federation/federatingdb/owns.go
@@ -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
}
diff --git a/internal/federation/federatingdb/undo.go b/internal/federation/federatingdb/undo.go
index 3feee6457..063adaedd 100644
--- a/internal/federation/federatingdb/undo.go
+++ b/internal/federation/federatingdb/undo.go
@@ -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
}
}
diff --git a/internal/federation/federatingdb/util.go b/internal/federation/federatingdb/util.go
index ed3c252d9..28f4c5a21 100644
--- a/internal/federation/federatingdb/util.go
+++ b/internal/federation/federatingdb/util.go
@@ -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
diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go
index c0943a328..1acdb6cb1 100644
--- a/internal/federation/federatingprotocol.go
+++ b/internal/federation/federatingprotocol.go
@@ -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
}
}
diff --git a/internal/gtsmodel/block.go b/internal/gtsmodel/block.go
index 27b39727c..32afede55 100644
--- a/internal/gtsmodel/block.go
+++ b/internal/gtsmodel/block.go
@@ -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"`
}
diff --git a/internal/gtsmodel/status.go b/internal/gtsmodel/status.go
index caa5a2a25..84b3dfc7c 100644
--- a/internal/gtsmodel/status.go
+++ b/internal/gtsmodel/status.go
@@ -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
diff --git a/internal/processing/account.go b/internal/processing/account.go
index ec58846d1..f722c88eb 100644
--- a/internal/processing/account.go
+++ b/internal/processing/account.go
@@ -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)
+}
diff --git a/internal/processing/account/account.go b/internal/processing/account/account.go
index efdac5d3e..7b8910149 100644
--- a/internal/processing/account/account.go
+++ b/internal/processing/account/account.go
@@ -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.
diff --git a/internal/processing/account/createblock.go b/internal/processing/account/createblock.go
new file mode 100644
index 000000000..79ce03805
--- /dev/null
+++ b/internal/processing/account/createblock.go
@@ -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 .
+*/
+
+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)
+}
diff --git a/internal/processing/account/get.go b/internal/processing/account/get.go
index aba1ed14a..b937ace5b 100644
--- a/internal/processing/account/get.go
+++ b/internal/processing/account/get.go
@@ -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)
diff --git a/internal/processing/account/removeblock.go b/internal/processing/account/removeblock.go
new file mode 100644
index 000000000..03b0c6750
--- /dev/null
+++ b/internal/processing/account/removeblock.go
@@ -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 .
+*/
+
+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)
+}
diff --git a/internal/processing/blocks.go b/internal/processing/blocks.go
new file mode 100644
index 000000000..509600ca6
--- /dev/null
+++ b/internal/processing/blocks.go
@@ -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 .
+*/
+
+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
+}
diff --git a/internal/processing/fromclientapi.go b/internal/processing/fromclientapi.go
index cfed3b5e4..6755a9d82 100644
--- a/internal/processing/fromclientapi.go
+++ b/internal/processing/fromclientapi.go
@@ -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
+}
diff --git a/internal/processing/fromfederator.go b/internal/processing/fromfederator.go
index 36568cf13..94a4e5af8 100644
--- a/internal/processing/fromfederator.go
+++ b/internal/processing/fromfederator.go
@@ -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)
}
diff --git a/internal/processing/processor.go b/internal/processing/processor.go
index bb4cd2da7..a09a370e9 100644
--- a/internal/processing/processor.go
+++ b/internal/processing/processor.go
@@ -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)
diff --git a/internal/timeline/get.go b/internal/timeline/get.go
index f07d81d55..d7ebb7766 100644
--- a/internal/timeline/get.go
+++ b/internal/timeline/get.go
@@ -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 .
+*/
+
package timeline
import (
diff --git a/internal/timeline/index.go b/internal/timeline/index.go
index 8c6b0d578..8dd7fee97 100644
--- a/internal/timeline/index.go
+++ b/internal/timeline/index.go
@@ -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 .
+*/
+
package timeline
import (
@@ -44,7 +62,7 @@ func (t *timeline) IndexBefore(statusID string, include bool, amount int) error
}
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 @@ func (t *timeline) IndexBehind(statusID string, amount int) error {
}
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)
diff --git a/internal/timeline/manager.go b/internal/timeline/manager.go
index d50c9f783..00d87bb26 100644
--- a/internal/timeline/manager.go
+++ b/internal/timeline/manager.go
@@ -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)
diff --git a/internal/timeline/postindex.go b/internal/timeline/postindex.go
index 44765bf50..db155d0fe 100644
--- a/internal/timeline/postindex.go
+++ b/internal/timeline/postindex.go
@@ -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 .
+*/
+
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) {
diff --git a/internal/timeline/prepare.go b/internal/timeline/prepare.go
index ac85d92e9..0fbd8ebba 100644
--- a/internal/timeline/prepare.go
+++ b/internal/timeline/prepare.go
@@ -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 .
+*/
+
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)
diff --git a/internal/timeline/preparedposts.go b/internal/timeline/preparedposts.go
index 1976189c8..7f8d84357 100644
--- a/internal/timeline/preparedposts.go
+++ b/internal/timeline/preparedposts.go
@@ -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 .
+*/
+
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 {
diff --git a/internal/timeline/remove.go b/internal/timeline/remove.go
index 8842c60cb..cf0b0b617 100644
--- a/internal/timeline/remove.go
+++ b/internal/timeline/remove.go
@@ -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 .
+*/
+
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
+}
diff --git a/internal/timeline/timeline.go b/internal/timeline/timeline.go
index d0fadb19e..fe811a303 100644
--- a/internal/timeline/timeline.go
+++ b/internal/timeline/timeline.go
@@ -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
diff --git a/internal/typeutils/asinterfaces.go b/internal/typeutils/asinterfaces.go
index aae3ecf93..d0b1cf617 100644
--- a/internal/typeutils/asinterfaces.go
+++ b/internal/typeutils/asinterfaces.go
@@ -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
diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go
index dc58346fb..394de6e82 100644
--- a/internal/typeutils/astointernal.go
+++ b/internal/typeutils/astointernal.go
@@ -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
diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go
index 30c1c7d2c..57c2a1f6d 100644
--- a/internal/typeutils/converter.go
+++ b/internal/typeutils/converter.go
@@ -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
diff --git a/internal/typeutils/internal.go b/internal/typeutils/internal.go
index b081708a2..a46ad7fbd 100644
--- a/internal/typeutils/internal.go
+++ b/internal/typeutils/internal.go
@@ -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,
diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go
index 1760b8731..809cedc45 100644
--- a/internal/typeutils/internaltoas.go
+++ b/internal/typeutils/internaltoas.go
@@ -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
+}
diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go
index 61c11b8ef..03e071981 100644
--- a/internal/typeutils/internaltofrontend.go
+++ b/internal/typeutils/internaltofrontend.go
@@ -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
}
diff --git a/internal/util/regexes.go b/internal/util/regexes.go
index 25d90417c..1ca34708f 100644
--- a/internal/util/regexes.go
+++ b/internal/util/regexes.go
@@ -104,4 +104,9 @@
// 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)
)
diff --git a/internal/util/uri.go b/internal/util/uri.go
index 5eb291628..370b2fa6f 100644
--- a/internal/util/uri.go
+++ b/internal/util/uri.go
@@ -50,6 +50,8 @@
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
+}