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)
|
* [ ] /api/v1/accounts/:id/identity_proofs GET (Get identity proofs for this account)
|
||||||
* [x] /api/v1/accounts/:id/follow POST (Follow this account)
|
* [x] /api/v1/accounts/:id/follow POST (Follow this account)
|
||||||
* [x] /api/v1/accounts/:id/unfollow POST (Unfollow this account)
|
* [x] /api/v1/accounts/:id/unfollow POST (Unfollow this account)
|
||||||
* [ ] /api/v1/accounts/:id/block POST (Block this account)
|
* [x] /api/v1/accounts/:id/block POST (Block this account)
|
||||||
* [ ] /api/v1/accounts/:id/unblock POST (Unblock this account)
|
* [x] /api/v1/accounts/:id/unblock POST (Unblock this account)
|
||||||
* [ ] /api/v1/accounts/:id/mute POST (Mute this account)
|
* [ ] /api/v1/accounts/:id/mute POST (Mute this account)
|
||||||
* [ ] /api/v1/accounts/:id/unmute POST (Unmute this account)
|
* [ ] /api/v1/accounts/:id/unmute POST (Unmute this account)
|
||||||
* [ ] /api/v1/accounts/:id/pin POST (Feature this account on profile)
|
* [ ] /api/v1/accounts/:id/pin POST (Feature this account on profile)
|
||||||
|
@ -71,8 +71,8 @@ Things are moving on the project! As of July 2021 you can now:
|
||||||
* [x] /api/v1/favourites GET (See faved statuses)
|
* [x] /api/v1/favourites GET (See faved statuses)
|
||||||
* [ ] Mutes
|
* [ ] Mutes
|
||||||
* [ ] /api/v1/mutes GET (See list of muted accounts)
|
* [ ] /api/v1/mutes GET (See list of muted accounts)
|
||||||
* [ ] Blocks
|
* [x] Blocks
|
||||||
* [ ] /api/v1/blocks GET (See list of blocked accounts)
|
* [x] /api/v1/blocks GET (See list of blocked accounts)
|
||||||
* [ ] Domain Blocks
|
* [ ] Domain Blocks
|
||||||
* [x] /api/v1/domain_blocks GET (See list of domain blocks)
|
* [x] /api/v1/domain_blocks GET (See list of domain blocks)
|
||||||
* [x] /api/v1/domain_blocks POST (Create a domain block)
|
* [x] /api/v1/domain_blocks POST (Create a domain block)
|
||||||
|
|
|
@ -61,10 +61,14 @@ const (
|
||||||
GetFollowingPath = BasePathWithID + "/following"
|
GetFollowingPath = BasePathWithID + "/following"
|
||||||
// GetRelationshipsPath is for showing an account's relationship with other accounts
|
// GetRelationshipsPath is for showing an account's relationship with other accounts
|
||||||
GetRelationshipsPath = BasePath + "/relationships"
|
GetRelationshipsPath = BasePath + "/relationships"
|
||||||
// PostFollowPath is for POSTing new follows to, and updating existing follows
|
// FollowPath is for POSTing new follows to, and updating existing follows
|
||||||
PostFollowPath = BasePathWithID + "/follow"
|
FollowPath = BasePathWithID + "/follow"
|
||||||
// PostUnfollowPath is for POSTing an unfollow
|
// UnfollowPath is for POSTing an unfollow
|
||||||
PostUnfollowPath = BasePathWithID + "/unfollow"
|
UnfollowPath = BasePathWithID + "/unfollow"
|
||||||
|
// BlockPath is for creating a block of an account
|
||||||
|
BlockPath = BasePathWithID + "/block"
|
||||||
|
// UnblockPath is for removing a block of an account
|
||||||
|
UnblockPath = BasePathWithID + "/unblock"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Module implements the ClientAPIModule interface for account-related actions
|
// Module implements the ClientAPIModule interface for account-related actions
|
||||||
|
@ -85,15 +89,33 @@ func New(config *config.Config, processor processing.Processor, log *logrus.Logg
|
||||||
|
|
||||||
// Route attaches all routes from this module to the given router
|
// Route attaches all routes from this module to the given router
|
||||||
func (m *Module) Route(r router.Router) error {
|
func (m *Module) Route(r router.Router) error {
|
||||||
|
// create account
|
||||||
r.AttachHandler(http.MethodPost, BasePath, m.AccountCreatePOSTHandler)
|
r.AttachHandler(http.MethodPost, BasePath, m.AccountCreatePOSTHandler)
|
||||||
|
|
||||||
|
// get account
|
||||||
r.AttachHandler(http.MethodGet, BasePathWithID, m.muxHandler)
|
r.AttachHandler(http.MethodGet, BasePathWithID, m.muxHandler)
|
||||||
|
|
||||||
|
// modify account
|
||||||
r.AttachHandler(http.MethodPatch, BasePathWithID, m.muxHandler)
|
r.AttachHandler(http.MethodPatch, BasePathWithID, m.muxHandler)
|
||||||
|
|
||||||
|
// get account's statuses
|
||||||
r.AttachHandler(http.MethodGet, GetStatusesPath, m.AccountStatusesGETHandler)
|
r.AttachHandler(http.MethodGet, GetStatusesPath, m.AccountStatusesGETHandler)
|
||||||
|
|
||||||
|
// get following or followers
|
||||||
r.AttachHandler(http.MethodGet, GetFollowersPath, m.AccountFollowersGETHandler)
|
r.AttachHandler(http.MethodGet, GetFollowersPath, m.AccountFollowersGETHandler)
|
||||||
r.AttachHandler(http.MethodGet, GetFollowingPath, m.AccountFollowingGETHandler)
|
r.AttachHandler(http.MethodGet, GetFollowingPath, m.AccountFollowingGETHandler)
|
||||||
|
|
||||||
|
// get relationship with account
|
||||||
r.AttachHandler(http.MethodGet, GetRelationshipsPath, m.AccountRelationshipsGETHandler)
|
r.AttachHandler(http.MethodGet, GetRelationshipsPath, m.AccountRelationshipsGETHandler)
|
||||||
r.AttachHandler(http.MethodPost, PostFollowPath, m.AccountFollowPOSTHandler)
|
|
||||||
r.AttachHandler(http.MethodPost, PostUnfollowPath, m.AccountUnfollowPOSTHandler)
|
// follow or unfollow account
|
||||||
|
r.AttachHandler(http.MethodPost, FollowPath, m.AccountFollowPOSTHandler)
|
||||||
|
r.AttachHandler(http.MethodPost, UnfollowPath, m.AccountUnfollowPOSTHandler)
|
||||||
|
|
||||||
|
// block or unblock account
|
||||||
|
r.AttachHandler(http.MethodPost, BlockPath, m.AccountBlockPOSTHandler)
|
||||||
|
r.AttachHandler(http.MethodPost, UnblockPath, m.AccountUnblockPOSTHandler)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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 {
|
select {
|
||||||
case m := <-stream.Messages:
|
case m := <-stream.Messages:
|
||||||
// we've got a streaming message!!
|
// we've got a streaming message!!
|
||||||
l.Debug("received message from stream")
|
l.Trace("received message from stream")
|
||||||
if err := conn.WriteJSON(m); err != nil {
|
if err := conn.WriteJSON(m); err != nil {
|
||||||
l.Infof("error writing json to websocket connection: %s", err)
|
l.Debugf("error writing json to websocket connection: %s", err)
|
||||||
// if something is wrong we want to bail and drop the connection -- the client will create a new one
|
// if something is wrong we want to bail and drop the connection -- the client will create a new one
|
||||||
break sendLoop
|
break sendLoop
|
||||||
}
|
}
|
||||||
l.Debug("wrote message into websocket connection")
|
l.Trace("wrote message into websocket connection")
|
||||||
case <-t.C:
|
case <-t.C:
|
||||||
l.Debug("received TICK from ticker")
|
l.Trace("received TICK from ticker")
|
||||||
if err := conn.WriteMessage(websocket.PingMessage, []byte(": ping")); err != nil {
|
if err := conn.WriteMessage(websocket.PingMessage, []byte(": ping")); err != nil {
|
||||||
l.Infof("error writing ping to websocket connection: %s", err)
|
l.Debugf("error writing ping to websocket connection: %s", err)
|
||||||
// if something is wrong we want to bail and drop the connection -- the client will create a new one
|
// if something is wrong we want to bail and drop the connection -- the client will create a new one
|
||||||
break sendLoop
|
break sendLoop
|
||||||
}
|
}
|
||||||
l.Debug("wrote ping message into websocket connection")
|
l.Trace("wrote ping message into websocket connection")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Debug("leaving StreamGETHandler")
|
l.Trace("leaving StreamGETHandler")
|
||||||
}
|
}
|
||||||
|
|
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/admin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/app"
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/app"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/auth"
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/auth"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/blocks"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/emoji"
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/emoji"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/favourites"
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/favourites"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver"
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver"
|
||||||
|
@ -143,6 +144,7 @@ var Start cliactions.GTSAction = func(ctx context.Context, c *config.Config, log
|
||||||
securityModule := security.New(c, dbService, log)
|
securityModule := security.New(c, dbService, log)
|
||||||
streamingModule := streaming.New(c, processor, log)
|
streamingModule := streaming.New(c, processor, log)
|
||||||
favouritesModule := favourites.New(c, processor, log)
|
favouritesModule := favourites.New(c, processor, log)
|
||||||
|
blocksModule := blocks.New(c, processor, log)
|
||||||
|
|
||||||
apis := []api.ClientModule{
|
apis := []api.ClientModule{
|
||||||
// modules with middleware go first
|
// modules with middleware go first
|
||||||
|
@ -170,6 +172,7 @@ var Start cliactions.GTSAction = func(ctx context.Context, c *config.Config, log
|
||||||
listsModule,
|
listsModule,
|
||||||
streamingModule,
|
streamingModule,
|
||||||
favouritesModule,
|
favouritesModule,
|
||||||
|
blocksModule,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, m := range apis {
|
for _, m := range apis {
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/admin"
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/admin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/app"
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/app"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/auth"
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/auth"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/blocks"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/emoji"
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/emoji"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/favourites"
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/favourites"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver"
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver"
|
||||||
|
@ -88,6 +89,7 @@ var Start cliactions.GTSAction = func(ctx context.Context, _ *config.Config, log
|
||||||
securityModule := security.New(c, dbService, log)
|
securityModule := security.New(c, dbService, log)
|
||||||
streamingModule := streaming.New(c, processor, log)
|
streamingModule := streaming.New(c, processor, log)
|
||||||
favouritesModule := favourites.New(c, processor, log)
|
favouritesModule := favourites.New(c, processor, log)
|
||||||
|
blocksModule := blocks.New(c, processor, log)
|
||||||
|
|
||||||
apis := []api.ClientModule{
|
apis := []api.ClientModule{
|
||||||
// modules with middleware go first
|
// modules with middleware go first
|
||||||
|
@ -115,6 +117,7 @@ var Start cliactions.GTSAction = func(ctx context.Context, _ *config.Config, log
|
||||||
listsModule,
|
listsModule,
|
||||||
streamingModule,
|
streamingModule,
|
||||||
favouritesModule,
|
favouritesModule,
|
||||||
|
blocksModule,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, m := range apis {
|
for _, m := range apis {
|
||||||
|
|
|
@ -159,6 +159,8 @@ type DB interface {
|
||||||
// In case of no entries, a 'no entries' error will be returned
|
// In case of no entries, a 'no entries' error will be returned
|
||||||
GetStatusesForAccount(accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, error)
|
GetStatusesForAccount(accountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]*gtsmodel.Status, error)
|
||||||
|
|
||||||
|
GetBlocksForAccount(accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, error)
|
||||||
|
|
||||||
// GetLastStatusForAccountID simply gets the most recent status by the given account.
|
// GetLastStatusForAccountID simply gets the most recent status by the given account.
|
||||||
// The given slice 'status' pointer will be set to the result of the query, whatever it is.
|
// The given slice 'status' pointer will be set to the result of the query, whatever it is.
|
||||||
// In case of no entries, a 'no entries' error will be returned
|
// In case of no entries, a 'no entries' error will be returned
|
||||||
|
|
67
internal/db/pg/blocks.go
Normal file
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.Language = boostedStatus.Language
|
||||||
announce.Text = boostedStatus.Text
|
announce.Text = boostedStatus.Text
|
||||||
announce.BoostOfID = boostedStatus.ID
|
announce.BoostOfID = boostedStatus.ID
|
||||||
|
announce.BoostOfAccountID = boostedStatus.AccountID
|
||||||
announce.Visibility = boostedStatus.Visibility
|
announce.Visibility = boostedStatus.Visibility
|
||||||
announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced
|
announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced
|
||||||
announce.GTSBoostedStatus = boostedStatus
|
announce.GTSBoostedStatus = boostedStatus
|
||||||
|
@ -477,6 +478,7 @@ func (f *federator) DereferenceAnnounce(announce *gtsmodel.Status, requestingUse
|
||||||
announce.Language = boostedStatus.Language
|
announce.Language = boostedStatus.Language
|
||||||
announce.Text = boostedStatus.Text
|
announce.Text = boostedStatus.Text
|
||||||
announce.BoostOfID = boostedStatus.ID
|
announce.BoostOfID = boostedStatus.ID
|
||||||
|
announce.BoostOfAccountID = boostedStatus.AccountID
|
||||||
announce.Visibility = boostedStatus.Visibility
|
announce.Visibility = boostedStatus.Visibility
|
||||||
announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced
|
announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced
|
||||||
announce.GTSBoostedStatus = boostedStatus
|
announce.GTSBoostedStatus = boostedStatus
|
||||||
|
|
|
@ -129,6 +129,7 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case gtsmodel.ActivityStreamsFollow:
|
case gtsmodel.ActivityStreamsFollow:
|
||||||
|
// FOLLOW SOMETHING
|
||||||
follow, ok := asType.(vocab.ActivityStreamsFollow)
|
follow, ok := asType.(vocab.ActivityStreamsFollow)
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("could not convert type to follow")
|
return errors.New("could not convert type to follow")
|
||||||
|
@ -156,6 +157,7 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
|
||||||
ReceivingAccount: targetAcct,
|
ReceivingAccount: targetAcct,
|
||||||
}
|
}
|
||||||
case gtsmodel.ActivityStreamsLike:
|
case gtsmodel.ActivityStreamsLike:
|
||||||
|
// LIKE SOMETHING
|
||||||
like, ok := asType.(vocab.ActivityStreamsLike)
|
like, ok := asType.(vocab.ActivityStreamsLike)
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("could not convert type to like")
|
return errors.New("could not convert type to like")
|
||||||
|
@ -182,6 +184,34 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
|
||||||
GTSModel: fave,
|
GTSModel: fave,
|
||||||
ReceivingAccount: targetAcct,
|
ReceivingAccount: targetAcct,
|
||||||
}
|
}
|
||||||
|
case gtsmodel.ActivityStreamsBlock:
|
||||||
|
// BLOCK SOMETHING
|
||||||
|
blockable, ok := asType.(vocab.ActivityStreamsBlock)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("could not convert type to block")
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := f.typeConverter.ASBlockToBlock(blockable)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not convert Block to gts model block")
|
||||||
|
}
|
||||||
|
|
||||||
|
newID, err := id.NewULID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
block.ID = newID
|
||||||
|
|
||||||
|
if err := f.db.Put(block); err != nil {
|
||||||
|
return fmt.Errorf("database error inserting block: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fromFederatorChan <- gtsmodel.FromFederator{
|
||||||
|
APObjectType: gtsmodel.ActivityStreamsBlock,
|
||||||
|
APActivityType: gtsmodel.ActivityStreamsCreate,
|
||||||
|
GTSModel: block,
|
||||||
|
ReceivingAccount: targetAcct,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,16 +39,15 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {
|
||||||
"id": id.String(),
|
"id": id.String(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
l.Debugf("entering OWNS function with id %s", id.String())
|
l.Tracef("entering OWNS function with id %s", id.String())
|
||||||
|
|
||||||
// if the id host isn't this instance host, we don't own this IRI
|
// if the id host isn't this instance host, we don't own this IRI
|
||||||
if id.Host != f.config.Host {
|
if id.Host != f.config.Host {
|
||||||
l.Debugf("we DO NOT own activity because the host is %s not %s", id.Host, f.config.Host)
|
l.Tracef("we DO NOT own activity because the host is %s not %s", id.Host, f.config.Host)
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// apparently it belongs to this host, so what *is* it?
|
// apparently it belongs to this host, so what *is* it?
|
||||||
|
|
||||||
// check if it's a status, eg /users/example_username/statuses/SOME_UUID_OF_A_STATUS
|
// check if it's a status, eg /users/example_username/statuses/SOME_UUID_OF_A_STATUS
|
||||||
if util.IsStatusesPath(id) {
|
if util.IsStatusesPath(id) {
|
||||||
_, uid, err := util.ParseStatusesPath(id)
|
_, uid, err := util.ParseStatusesPath(id)
|
||||||
|
@ -63,11 +62,10 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {
|
||||||
// an actual error happened
|
// an actual error happened
|
||||||
return false, fmt.Errorf("database error fetching status with id %s: %s", uid, err)
|
return false, fmt.Errorf("database error fetching status with id %s: %s", uid, err)
|
||||||
}
|
}
|
||||||
l.Debug("we DO own this")
|
l.Debugf("we own url %s", id.String())
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if it's a user, eg /users/example_username
|
|
||||||
if util.IsUserPath(id) {
|
if util.IsUserPath(id) {
|
||||||
username, err := util.ParseUserPath(id)
|
username, err := util.ParseUserPath(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -81,7 +79,7 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {
|
||||||
// an actual error happened
|
// an actual error happened
|
||||||
return false, fmt.Errorf("database error fetching account with username %s: %s", username, err)
|
return false, fmt.Errorf("database error fetching account with username %s: %s", username, err)
|
||||||
}
|
}
|
||||||
l.Debug("we DO own this")
|
l.Debugf("we own url %s", id.String())
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,7 +96,7 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {
|
||||||
// an actual error happened
|
// an actual error happened
|
||||||
return false, fmt.Errorf("database error fetching account with username %s: %s", username, err)
|
return false, fmt.Errorf("database error fetching account with username %s: %s", username, err)
|
||||||
}
|
}
|
||||||
l.Debug("we DO own this")
|
l.Debugf("we own url %s", id.String())
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,7 +113,57 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {
|
||||||
// an actual error happened
|
// an actual error happened
|
||||||
return false, fmt.Errorf("database error fetching account with username %s: %s", username, err)
|
return false, fmt.Errorf("database error fetching account with username %s: %s", username, err)
|
||||||
}
|
}
|
||||||
l.Debug("we DO own this")
|
l.Debugf("we own url %s", id.String())
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if util.IsLikePath(id) {
|
||||||
|
username, likeID, err := util.ParseLikedPath(id)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("error parsing like path for url %s: %s", id.String(), err)
|
||||||
|
}
|
||||||
|
if err := f.db.GetLocalAccountByUsername(username, >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
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -85,6 +85,31 @@ func (f *federatingDB) Undo(ctx context.Context, undo vocab.ActivityStreamsUndo)
|
||||||
// UNDO LIKE
|
// UNDO LIKE
|
||||||
case string(gtsmodel.ActivityStreamsAnnounce):
|
case string(gtsmodel.ActivityStreamsAnnounce):
|
||||||
// UNDO BOOST/REBLOG/ANNOUNCE
|
// UNDO BOOST/REBLOG/ANNOUNCE
|
||||||
|
case string(gtsmodel.ActivityStreamsBlock):
|
||||||
|
// UNDO BLOCK
|
||||||
|
ASBlock, ok := iter.GetType().(vocab.ActivityStreamsBlock)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("UNDO: couldn't parse block into vocab.ActivityStreamsBlock")
|
||||||
|
}
|
||||||
|
// make sure the actor owns the follow
|
||||||
|
if !sameActor(undo.GetActivityStreamsActor(), ASBlock.GetActivityStreamsActor()) {
|
||||||
|
return errors.New("UNDO: block actor and activity actor not the same")
|
||||||
|
}
|
||||||
|
// convert the block to something we can understand
|
||||||
|
gtsBlock, err := f.typeConverter.ASBlockToBlock(ASBlock)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("UNDO: error converting asblock to gtsblock: %s", err)
|
||||||
|
}
|
||||||
|
// make sure the addressee of the original block is the same as whatever inbox this landed in
|
||||||
|
if gtsBlock.TargetAccountID != targetAcct.ID {
|
||||||
|
return errors.New("UNDO: block object account and inbox account were not the same")
|
||||||
|
}
|
||||||
|
// delete any existing BLOCK
|
||||||
|
if err := f.db.DeleteWhere([]db.Where{{Key: "uri", Value: gtsBlock.URI}}, >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
|
// ID might already be set on an announce we've created, so check it here and return it if it is
|
||||||
announce, ok := t.(vocab.ActivityStreamsAnnounce)
|
announce, ok := t.(vocab.ActivityStreamsAnnounce)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("newid: fave couldn't be parsed into vocab.ActivityStreamsAnnounce")
|
return nil, errors.New("newid: announce couldn't be parsed into vocab.ActivityStreamsAnnounce")
|
||||||
}
|
}
|
||||||
idProp := announce.GetJSONLDId()
|
idProp := announce.GetJSONLDId()
|
||||||
if idProp != nil {
|
if idProp != nil {
|
||||||
|
@ -152,7 +152,7 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (idURL *url.URL, e
|
||||||
// ID might already be set on an update we've created, so check it here and return it if it is
|
// ID might already be set on an update we've created, so check it here and return it if it is
|
||||||
update, ok := t.(vocab.ActivityStreamsUpdate)
|
update, ok := t.(vocab.ActivityStreamsUpdate)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("newid: fave couldn't be parsed into vocab.ActivityStreamsUpdate")
|
return nil, errors.New("newid: update couldn't be parsed into vocab.ActivityStreamsUpdate")
|
||||||
}
|
}
|
||||||
idProp := update.GetJSONLDId()
|
idProp := update.GetJSONLDId()
|
||||||
if idProp != nil {
|
if idProp != nil {
|
||||||
|
@ -160,6 +160,32 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (idURL *url.URL, e
|
||||||
return idProp.GetIRI(), nil
|
return idProp.GetIRI(), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case gtsmodel.ActivityStreamsBlock:
|
||||||
|
// BLOCK
|
||||||
|
// ID might already be set on a block we've created, so check it here and return it if it is
|
||||||
|
block, ok := t.(vocab.ActivityStreamsBlock)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("newid: block couldn't be parsed into vocab.ActivityStreamsBlock")
|
||||||
|
}
|
||||||
|
idProp := block.GetJSONLDId()
|
||||||
|
if idProp != nil {
|
||||||
|
if idProp.IsIRI() {
|
||||||
|
return idProp.GetIRI(), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case gtsmodel.ActivityStreamsUndo:
|
||||||
|
// UNDO
|
||||||
|
// ID might already be set on an undo we've created, so check it here and return it if it is
|
||||||
|
undo, ok := t.(vocab.ActivityStreamsUndo)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("newid: undo couldn't be parsed into vocab.ActivityStreamsUndo")
|
||||||
|
}
|
||||||
|
idProp := undo.GetJSONLDId()
|
||||||
|
if idProp != nil {
|
||||||
|
if idProp.IsIRI() {
|
||||||
|
return idProp.GetIRI(), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallback default behavior: just return a random ULID after our protocol and host
|
// fallback default behavior: just return a random ULID after our protocol and host
|
||||||
|
|
|
@ -243,8 +243,8 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
a := >smodel.Account{}
|
requestingAccount := >smodel.Account{}
|
||||||
if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, a); err != nil {
|
if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, requestingAccount); err != nil {
|
||||||
_, ok := err.(db.ErrNoEntries)
|
_, ok := err.(db.ErrNoEntries)
|
||||||
if ok {
|
if ok {
|
||||||
// we don't have an entry for this account so it's not blocked
|
// we don't have an entry for this account so it's not blocked
|
||||||
|
@ -253,11 +253,13 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er
|
||||||
}
|
}
|
||||||
return false, fmt.Errorf("error getting account with uri %s: %s", uri.String(), err)
|
return false, fmt.Errorf("error getting account with uri %s: %s", uri.String(), err)
|
||||||
}
|
}
|
||||||
blocked, err := f.db.Blocked(requestedAccount.ID, a.ID)
|
|
||||||
if err != nil {
|
// check if requested account blocks requesting account
|
||||||
return false, fmt.Errorf("error checking account blocks: %s", err)
|
if err := f.db.GetWhere([]db.Where{
|
||||||
}
|
{Key: "account_id", Value: requestedAccount.ID},
|
||||||
if blocked {
|
{Key: "target_account_id", Value: requestingAccount.ID},
|
||||||
|
}, >smodel.Block{}); err == nil {
|
||||||
|
// a block exists
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,11 @@ type Block struct {
|
||||||
// When was this block updated
|
// When was this block updated
|
||||||
UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
||||||
// Who created this block?
|
// Who created this block?
|
||||||
AccountID string `pg:"type:CHAR(26),notnull"`
|
AccountID string `pg:"type:CHAR(26),notnull"`
|
||||||
|
Account *Account `pg:"rel:has-one"`
|
||||||
// Who is targeted by this block?
|
// Who is targeted by this block?
|
||||||
TargetAccountID string `pg:"type:CHAR(26),notnull"`
|
TargetAccountID string `pg:"type:CHAR(26),notnull"`
|
||||||
|
TargetAccount *Account `pg:"rel:has-one"`
|
||||||
// Activitypub URI for this block
|
// Activitypub URI for this block
|
||||||
URI string
|
URI string `pg:",notnull"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,8 @@ type Status struct {
|
||||||
InReplyToAccountID string `pg:"type:CHAR(26)"`
|
InReplyToAccountID string `pg:"type:CHAR(26)"`
|
||||||
// id of the status this status is a boost of
|
// id of the status this status is a boost of
|
||||||
BoostOfID string `pg:"type:CHAR(26)"`
|
BoostOfID string `pg:"type:CHAR(26)"`
|
||||||
|
// id of the account that owns the boosted status
|
||||||
|
BoostOfAccountID string `pg:"type:CHAR(26)"`
|
||||||
// cw string for this status
|
// cw string for this status
|
||||||
ContentWarning string
|
ContentWarning string
|
||||||
// visibility entry for this status
|
// visibility entry for this status
|
||||||
|
|
|
@ -59,3 +59,11 @@ func (p *processor) AccountFollowCreate(authed *oauth.Auth, form *apimodel.Accou
|
||||||
func (p *processor) AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
|
func (p *processor) AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
|
||||||
return p.accountProcessor.FollowRemove(authed.Account, targetAccountID)
|
return p.accountProcessor.FollowRemove(authed.Account, targetAccountID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *processor) AccountBlockCreate(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
|
||||||
|
return p.accountProcessor.BlockCreate(authed.Account, targetAccountID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *processor) AccountBlockRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
|
||||||
|
return p.accountProcessor.BlockRemove(authed.Account, targetAccountID)
|
||||||
|
}
|
||||||
|
|
|
@ -59,6 +59,11 @@ type Processor interface {
|
||||||
FollowCreate(requestingAccount *gtsmodel.Account, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode)
|
FollowCreate(requestingAccount *gtsmodel.Account, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode)
|
||||||
// FollowRemove handles the removal of a follow/follow request to an account, either remote or local.
|
// FollowRemove handles the removal of a follow/follow request to an account, either remote or local.
|
||||||
FollowRemove(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
|
FollowRemove(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
|
||||||
|
// BlockCreate handles the creation of a block from requestingAccount to targetAccountID, either remote or local.
|
||||||
|
BlockCreate(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
|
||||||
|
// BlockRemove handles the removal of a block from requestingAccount to targetAccountID, either remote or local.
|
||||||
|
BlockRemove(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
|
||||||
|
|
||||||
// UpdateHeader does the dirty work of checking the header part of an account update form,
|
// UpdateHeader does the dirty work of checking the header part of an account update form,
|
||||||
// parsing and checking the image, and doing the necessary updates in the database for this to become
|
// parsing and checking the image, and doing the necessary updates in the database for this to become
|
||||||
// the account's new header image.
|
// the account's new header image.
|
||||||
|
|
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)
|
p.log.WithField("func", "AccountGet").Debugf("dereferencing account: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var mastoAccount *apimodel.Account
|
var blocked bool
|
||||||
var err error
|
var err error
|
||||||
if requestingAccount != nil && targetAccount.ID == requestingAccount.ID {
|
if requestingAccount != nil {
|
||||||
|
blocked, err = p.db.Blocked(requestingAccount.ID, targetAccountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error checking account block: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var mastoAccount *apimodel.Account
|
||||||
|
if blocked {
|
||||||
|
mastoAccount, err = p.tc.AccountToMastoBlocked(targetAccount)
|
||||||
|
} else if requestingAccount != nil && targetAccount.ID == requestingAccount.ID {
|
||||||
mastoAccount, err = p.tc.AccountToMastoSensitive(targetAccount)
|
mastoAccount, err = p.tc.AccountToMastoSensitive(targetAccount)
|
||||||
} else {
|
} else {
|
||||||
mastoAccount, err = p.tc.AccountToMastoPublic(targetAccount)
|
mastoAccount, err = p.tc.AccountToMastoPublic(targetAccount)
|
||||||
|
|
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)
|
return p.federateFave(fave, clientMsg.OriginAccount, clientMsg.TargetAccount)
|
||||||
|
|
||||||
case gtsmodel.ActivityStreamsAnnounce:
|
case gtsmodel.ActivityStreamsAnnounce:
|
||||||
// CREATE BOOST/ANNOUNCE
|
// CREATE BOOST/ANNOUNCE
|
||||||
boostWrapperStatus, ok := clientMsg.GTSModel.(*gtsmodel.Status)
|
boostWrapperStatus, ok := clientMsg.GTSModel.(*gtsmodel.Status)
|
||||||
|
@ -93,6 +92,25 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.federateAnnounce(boostWrapperStatus, clientMsg.OriginAccount, clientMsg.TargetAccount)
|
return p.federateAnnounce(boostWrapperStatus, clientMsg.OriginAccount, clientMsg.TargetAccount)
|
||||||
|
case gtsmodel.ActivityStreamsBlock:
|
||||||
|
// CREATE BLOCK
|
||||||
|
block, ok := clientMsg.GTSModel.(*gtsmodel.Block)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("block was not parseable as *gtsmodel.Block")
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove any of the blocking account's statuses from the blocked account's timeline, and vice versa
|
||||||
|
if err := p.timelineManager.WipeStatusesFromAccountID(block.AccountID, block.TargetAccountID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := p.timelineManager.WipeStatusesFromAccountID(block.TargetAccountID, block.AccountID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: same with notifications
|
||||||
|
// TODO: same with bookmarks
|
||||||
|
|
||||||
|
return p.federateBlock(block)
|
||||||
}
|
}
|
||||||
case gtsmodel.ActivityStreamsUpdate:
|
case gtsmodel.ActivityStreamsUpdate:
|
||||||
// UPDATE
|
// UPDATE
|
||||||
|
@ -132,6 +150,13 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error
|
||||||
return errors.New("undo was not parseable as *gtsmodel.Follow")
|
return errors.New("undo was not parseable as *gtsmodel.Follow")
|
||||||
}
|
}
|
||||||
return p.federateUnfollow(follow, clientMsg.OriginAccount, clientMsg.TargetAccount)
|
return p.federateUnfollow(follow, clientMsg.OriginAccount, clientMsg.TargetAccount)
|
||||||
|
case gtsmodel.ActivityStreamsBlock:
|
||||||
|
// UNDO BLOCK
|
||||||
|
block, ok := clientMsg.GTSModel.(*gtsmodel.Block)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("undo was not parseable as *gtsmodel.Block")
|
||||||
|
}
|
||||||
|
return p.federateUnblock(block)
|
||||||
case gtsmodel.ActivityStreamsLike:
|
case gtsmodel.ActivityStreamsLike:
|
||||||
// UNDO LIKE/FAVE
|
// UNDO LIKE/FAVE
|
||||||
fave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave)
|
fave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave)
|
||||||
|
@ -530,3 +555,93 @@ func (p *processor) federateAccountUpdate(updatedAccount *gtsmodel.Account, orig
|
||||||
_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, update)
|
_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, update)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *processor) federateBlock(block *gtsmodel.Block) error {
|
||||||
|
if block.Account == nil {
|
||||||
|
a := >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),
|
"federatorMsg": fmt.Sprintf("%+v", federatorMsg),
|
||||||
})
|
})
|
||||||
|
|
||||||
l.Debug("entering function PROCESS FROM FEDERATOR")
|
l.Trace("entering function PROCESS FROM FEDERATOR")
|
||||||
|
|
||||||
switch federatorMsg.APActivityType {
|
switch federatorMsg.APActivityType {
|
||||||
case gtsmodel.ActivityStreamsCreate:
|
case gtsmodel.ActivityStreamsCreate:
|
||||||
|
@ -47,7 +47,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
|
||||||
return errors.New("note was not parseable as *gtsmodel.Status")
|
return errors.New("note was not parseable as *gtsmodel.Status")
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Debug("will now derefence incoming status")
|
l.Trace("will now derefence incoming status")
|
||||||
if err := p.federator.DereferenceStatusFields(incomingStatus, federatorMsg.ReceivingAccount.Username); err != nil {
|
if err := p.federator.DereferenceStatusFields(incomingStatus, federatorMsg.ReceivingAccount.Username); err != nil {
|
||||||
return fmt.Errorf("error dereferencing status from federator: %s", err)
|
return fmt.Errorf("error dereferencing status from federator: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -70,7 +70,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
|
||||||
return errors.New("profile was not parseable as *gtsmodel.Account")
|
return errors.New("profile was not parseable as *gtsmodel.Account")
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Debug("will now derefence incoming account")
|
l.Trace("will now derefence incoming account")
|
||||||
if err := p.federator.DereferenceAccountFields(incomingAccount, "", false); err != nil {
|
if err := p.federator.DereferenceAccountFields(incomingAccount, "", false); err != nil {
|
||||||
return fmt.Errorf("error dereferencing account from federator: %s", err)
|
return fmt.Errorf("error dereferencing account from federator: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -127,6 +127,22 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
|
||||||
if err := p.notifyAnnounce(incomingAnnounce); err != nil {
|
if err := p.notifyAnnounce(incomingAnnounce); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
case gtsmodel.ActivityStreamsBlock:
|
||||||
|
// CREATE A BLOCK
|
||||||
|
block, ok := federatorMsg.GTSModel.(*gtsmodel.Block)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("block was not parseable as *gtsmodel.Block")
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove any of the blocking account's statuses from the blocked account's timeline, and vice versa
|
||||||
|
if err := p.timelineManager.WipeStatusesFromAccountID(block.AccountID, block.TargetAccountID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := p.timelineManager.WipeStatusesFromAccountID(block.TargetAccountID, block.AccountID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// TODO: same with notifications
|
||||||
|
// TODO: same with bookmarks
|
||||||
}
|
}
|
||||||
case gtsmodel.ActivityStreamsUpdate:
|
case gtsmodel.ActivityStreamsUpdate:
|
||||||
// UPDATE
|
// UPDATE
|
||||||
|
@ -138,7 +154,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
|
||||||
return errors.New("profile was not parseable as *gtsmodel.Account")
|
return errors.New("profile was not parseable as *gtsmodel.Account")
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Debug("will now derefence incoming account")
|
l.Trace("will now derefence incoming account")
|
||||||
if err := p.federator.DereferenceAccountFields(incomingAccount, federatorMsg.ReceivingAccount.Username, true); err != nil {
|
if err := p.federator.DereferenceAccountFields(incomingAccount, federatorMsg.ReceivingAccount.Username, true); err != nil {
|
||||||
return fmt.Errorf("error dereferencing account from federator: %s", err)
|
return fmt.Errorf("error dereferencing account from federator: %s", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,6 +82,10 @@ type Processor interface {
|
||||||
AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode)
|
AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode)
|
||||||
// AccountFollowRemove handles the removal of a follow/follow request to an account, either remote or local.
|
// AccountFollowRemove handles the removal of a follow/follow request to an account, either remote or local.
|
||||||
AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
|
AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
|
||||||
|
// AccountBlockCreate handles the creation of a block from authed account to target account, either remote or local.
|
||||||
|
AccountBlockCreate(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
|
||||||
|
// AccountBlockRemove handles the removal of a block from authed account to target account, either remote or local.
|
||||||
|
AccountBlockRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
|
||||||
|
|
||||||
// AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form.
|
// AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form.
|
||||||
AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error)
|
AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error)
|
||||||
|
@ -99,6 +103,9 @@ type Processor interface {
|
||||||
// AppCreate processes the creation of a new API application
|
// AppCreate processes the creation of a new API application
|
||||||
AppCreate(authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, error)
|
AppCreate(authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, error)
|
||||||
|
|
||||||
|
// BlocksGet returns a list of accounts blocked by the requesting account.
|
||||||
|
BlocksGet(authed *oauth.Auth, maxID string, sinceID string, limit int) (*apimodel.BlocksResponse, gtserror.WithCode)
|
||||||
|
|
||||||
// FileGet handles the fetching of a media attachment file via the fileserver.
|
// FileGet handles the fetching of a media attachment file via the fileserver.
|
||||||
FileGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error)
|
FileGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error)
|
||||||
|
|
||||||
|
@ -275,14 +282,14 @@ func (p *processor) Start() error {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case clientMsg := <-p.fromClientAPI:
|
case clientMsg := <-p.fromClientAPI:
|
||||||
p.log.Infof("received message FROM client API: %+v", clientMsg)
|
p.log.Tracef("received message FROM client API: %+v", clientMsg)
|
||||||
go func() {
|
go func() {
|
||||||
if err := p.processFromClientAPI(clientMsg); err != nil {
|
if err := p.processFromClientAPI(clientMsg); err != nil {
|
||||||
p.log.Error(err)
|
p.log.Error(err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
case federatorMsg := <-p.fromFederator:
|
case federatorMsg := <-p.fromFederator:
|
||||||
p.log.Infof("received message FROM federator: %+v", federatorMsg)
|
p.log.Tracef("received message FROM federator: %+v", federatorMsg)
|
||||||
go func() {
|
go func() {
|
||||||
if err := p.processFromFederator(federatorMsg); err != nil {
|
if err := p.processFromFederator(federatorMsg); err != nil {
|
||||||
p.log.Error(err)
|
p.log.Error(err)
|
||||||
|
|
|
@ -1,3 +1,21 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package timeline
|
package timeline
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
@ -1,3 +1,21 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package timeline
|
package timeline
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -44,7 +62,7 @@ grabloop:
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range filtered {
|
for _, s := range filtered {
|
||||||
if _, err := t.IndexOne(s.CreatedAt, s.ID, s.BoostOfID); err != nil {
|
if _, err := t.IndexOne(s.CreatedAt, s.ID, s.BoostOfID, s.AccountID, s.BoostOfAccountID); err != nil {
|
||||||
return fmt.Errorf("IndexBefore: error indexing status with id %s: %s", s.ID, err)
|
return fmt.Errorf("IndexBefore: error indexing status with id %s: %s", s.ID, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,7 +97,7 @@ grabloop:
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range filtered {
|
for _, s := range filtered {
|
||||||
if _, err := t.IndexOne(s.CreatedAt, s.ID, s.BoostOfID); err != nil {
|
if _, err := t.IndexOne(s.CreatedAt, s.ID, s.BoostOfID, s.AccountID, s.BoostOfAccountID); err != nil {
|
||||||
return fmt.Errorf("IndexBehind: error indexing status with id %s: %s", s.ID, err)
|
return fmt.Errorf("IndexBehind: error indexing status with id %s: %s", s.ID, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,24 +109,29 @@ func (t *timeline) IndexOneByID(statusID string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *timeline) IndexOne(statusCreatedAt time.Time, statusID string, boostOfID string) (bool, error) {
|
func (t *timeline) IndexOne(statusCreatedAt time.Time, statusID string, boostOfID string, accountID string, boostOfAccountID string) (bool, error) {
|
||||||
t.Lock()
|
t.Lock()
|
||||||
defer t.Unlock()
|
defer t.Unlock()
|
||||||
|
|
||||||
postIndexEntry := &postIndexEntry{
|
postIndexEntry := &postIndexEntry{
|
||||||
statusID: statusID,
|
statusID: statusID,
|
||||||
boostOfID: boostOfID,
|
boostOfID: boostOfID,
|
||||||
|
accountID: accountID,
|
||||||
|
boostOfAccountID: boostOfAccountID,
|
||||||
}
|
}
|
||||||
|
|
||||||
return t.postIndex.insertIndexed(postIndexEntry)
|
return t.postIndex.insertIndexed(postIndexEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *timeline) IndexAndPrepareOne(statusCreatedAt time.Time, statusID string) (bool, error) {
|
func (t *timeline) IndexAndPrepareOne(statusCreatedAt time.Time, statusID string, boostOfID string, accountID string, boostOfAccountID string) (bool, error) {
|
||||||
t.Lock()
|
t.Lock()
|
||||||
defer t.Unlock()
|
defer t.Unlock()
|
||||||
|
|
||||||
postIndexEntry := &postIndexEntry{
|
postIndexEntry := &postIndexEntry{
|
||||||
statusID: statusID,
|
statusID: statusID,
|
||||||
|
boostOfID: boostOfID,
|
||||||
|
accountID: accountID,
|
||||||
|
boostOfAccountID: boostOfAccountID,
|
||||||
}
|
}
|
||||||
|
|
||||||
inserted, err := t.postIndex.insertIndexed(postIndexEntry)
|
inserted, err := t.postIndex.insertIndexed(postIndexEntry)
|
||||||
|
|
|
@ -78,6 +78,8 @@ type Manager interface {
|
||||||
Remove(statusID string, timelineAccountID string) (int, error)
|
Remove(statusID string, timelineAccountID string) (int, error)
|
||||||
// WipeStatusFromAllTimelines removes one status from the index and prepared posts of all timelines
|
// WipeStatusFromAllTimelines removes one status from the index and prepared posts of all timelines
|
||||||
WipeStatusFromAllTimelines(statusID string) error
|
WipeStatusFromAllTimelines(statusID string) error
|
||||||
|
// WipeStatusesFromAccountID removes all statuses by the given accountID from the timelineAccountID's timelines.
|
||||||
|
WipeStatusesFromAccountID(accountID string, timelineAccountID string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewManager returns a new timeline manager with the given database, typeconverter, config, and log.
|
// NewManager returns a new timeline manager with the given database, typeconverter, config, and log.
|
||||||
|
@ -112,7 +114,7 @@ func (m *manager) Ingest(status *gtsmodel.Status, timelineAccountID string) (boo
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Trace("ingesting status")
|
l.Trace("ingesting status")
|
||||||
return t.IndexOne(status.CreatedAt, status.ID, status.BoostOfID)
|
return t.IndexOne(status.CreatedAt, status.ID, status.BoostOfID, status.AccountID, status.BoostOfAccountID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *manager) IngestAndPrepare(status *gtsmodel.Status, timelineAccountID string) (bool, error) {
|
func (m *manager) IngestAndPrepare(status *gtsmodel.Status, timelineAccountID string) (bool, error) {
|
||||||
|
@ -128,7 +130,7 @@ func (m *manager) IngestAndPrepare(status *gtsmodel.Status, timelineAccountID st
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Trace("ingesting status")
|
l.Trace("ingesting status")
|
||||||
return t.IndexAndPrepareOne(status.CreatedAt, status.ID)
|
return t.IndexAndPrepareOne(status.CreatedAt, status.ID, status.BoostOfID, status.AccountID, status.BoostOfAccountID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *manager) Remove(statusID string, timelineAccountID string) (int, error) {
|
func (m *manager) Remove(statusID string, timelineAccountID string) (int, error) {
|
||||||
|
@ -219,6 +221,16 @@ func (m *manager) WipeStatusFromAllTimelines(statusID string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *manager) WipeStatusesFromAccountID(accountID string, timelineAccountID string) error {
|
||||||
|
t, err := m.getOrCreateTimeline(timelineAccountID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = t.RemoveAllBy(accountID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (m *manager) getOrCreateTimeline(timelineAccountID string) (Timeline, error) {
|
func (m *manager) getOrCreateTimeline(timelineAccountID string) (Timeline, error) {
|
||||||
var t Timeline
|
var t Timeline
|
||||||
i, ok := m.accountTimelines.Load(timelineAccountID)
|
i, ok := m.accountTimelines.Load(timelineAccountID)
|
||||||
|
|
|
@ -1,3 +1,21 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package timeline
|
package timeline
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -10,8 +28,10 @@ type postIndex struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type postIndexEntry struct {
|
type postIndexEntry struct {
|
||||||
statusID string
|
statusID string
|
||||||
boostOfID string
|
boostOfID string
|
||||||
|
accountID string
|
||||||
|
boostOfAccountID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *postIndex) insertIndexed(i *postIndexEntry) (bool, error) {
|
func (p *postIndex) insertIndexed(i *postIndexEntry) (bool, error) {
|
||||||
|
|
|
@ -1,3 +1,21 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package timeline
|
package timeline
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -207,8 +225,11 @@ func (t *timeline) prepare(statusID string) error {
|
||||||
|
|
||||||
// shove it in prepared posts as a prepared posts entry
|
// shove it in prepared posts as a prepared posts entry
|
||||||
preparedPostsEntry := &preparedPostsEntry{
|
preparedPostsEntry := &preparedPostsEntry{
|
||||||
statusID: statusID,
|
statusID: gtsStatus.ID,
|
||||||
prepared: apiModelStatus,
|
boostOfID: gtsStatus.BoostOfID,
|
||||||
|
accountID: gtsStatus.AccountID,
|
||||||
|
boostOfAccountID: gtsStatus.BoostOfAccountID,
|
||||||
|
prepared: apiModelStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
return t.preparedPosts.insertPrepared(preparedPostsEntry)
|
return t.preparedPosts.insertPrepared(preparedPostsEntry)
|
||||||
|
|
|
@ -1,3 +1,21 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package timeline
|
package timeline
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -12,8 +30,11 @@ type preparedPosts struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type preparedPostsEntry struct {
|
type preparedPostsEntry struct {
|
||||||
statusID string
|
statusID string
|
||||||
prepared *apimodel.Status
|
boostOfID string
|
||||||
|
accountID string
|
||||||
|
boostOfAccountID string
|
||||||
|
prepared *apimodel.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *preparedPosts) insertPrepared(i *preparedPostsEntry) error {
|
func (p *preparedPosts) insertPrepared(i *preparedPostsEntry) error {
|
||||||
|
|
|
@ -1,3 +1,21 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package timeline
|
package timeline
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -58,3 +76,55 @@ func (t *timeline) Remove(statusID string) (int, error) {
|
||||||
l.Debugf("removed %d entries", removed)
|
l.Debugf("removed %d entries", removed)
|
||||||
return removed, nil
|
return removed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *timeline) RemoveAllBy(accountID string) (int, error) {
|
||||||
|
l := t.log.WithFields(logrus.Fields{
|
||||||
|
"func": "RemoveAllBy",
|
||||||
|
"accountTimeline": t.accountID,
|
||||||
|
"accountID": accountID,
|
||||||
|
})
|
||||||
|
t.Lock()
|
||||||
|
defer t.Unlock()
|
||||||
|
var removed int
|
||||||
|
|
||||||
|
// remove entr(ies) from the post index
|
||||||
|
removeIndexes := []*list.Element{}
|
||||||
|
if t.postIndex != nil && t.postIndex.data != nil {
|
||||||
|
for e := t.postIndex.data.Front(); e != nil; e = e.Next() {
|
||||||
|
entry, ok := e.Value.(*postIndexEntry)
|
||||||
|
if !ok {
|
||||||
|
return removed, errors.New("Remove: could not parse e as a postIndexEntry")
|
||||||
|
}
|
||||||
|
if entry.accountID == accountID || entry.boostOfAccountID == accountID {
|
||||||
|
l.Debug("found status in postIndex")
|
||||||
|
removeIndexes = append(removeIndexes, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, e := range removeIndexes {
|
||||||
|
t.postIndex.data.Remove(e)
|
||||||
|
removed = removed + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove entr(ies) from prepared posts
|
||||||
|
removePrepared := []*list.Element{}
|
||||||
|
if t.preparedPosts != nil && t.preparedPosts.data != nil {
|
||||||
|
for e := t.preparedPosts.data.Front(); e != nil; e = e.Next() {
|
||||||
|
entry, ok := e.Value.(*preparedPostsEntry)
|
||||||
|
if !ok {
|
||||||
|
return removed, errors.New("Remove: could not parse e as a preparedPostsEntry")
|
||||||
|
}
|
||||||
|
if entry.accountID == accountID || entry.boostOfAccountID == accountID {
|
||||||
|
l.Debug("found status in preparedPosts")
|
||||||
|
removePrepared = append(removePrepared, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, e := range removePrepared {
|
||||||
|
t.preparedPosts.data.Remove(e)
|
||||||
|
removed = removed + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Debugf("removed %d entries", removed)
|
||||||
|
return removed, nil
|
||||||
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ type Timeline interface {
|
||||||
//
|
//
|
||||||
// The returned bool indicates whether or not the status was actually inserted into the timeline. This will be false
|
// The returned bool indicates whether or not the status was actually inserted into the timeline. This will be false
|
||||||
// if the status is a boost and the original post or another boost of it already exists < boostReinsertionDepth back in the timeline.
|
// if the status is a boost and the original post or another boost of it already exists < boostReinsertionDepth back in the timeline.
|
||||||
IndexOne(statusCreatedAt time.Time, statusID string, boostOfID string) (bool, error)
|
IndexOne(statusCreatedAt time.Time, statusID string, boostOfID string, accountID string, boostOfAccountID string) (bool, error)
|
||||||
|
|
||||||
// OldestIndexedPostID returns the id of the rearmost (ie., the oldest) indexed post, or an error if something goes wrong.
|
// OldestIndexedPostID returns the id of the rearmost (ie., the oldest) indexed post, or an error if something goes wrong.
|
||||||
// If nothing goes wrong but there's no oldest post, an empty string will be returned so make sure to check for this.
|
// If nothing goes wrong but there's no oldest post, an empty string will be returned so make sure to check for this.
|
||||||
|
@ -85,7 +85,7 @@ type Timeline interface {
|
||||||
//
|
//
|
||||||
// The returned bool indicates whether or not the status was actually inserted into the timeline. This will be false
|
// The returned bool indicates whether or not the status was actually inserted into the timeline. This will be false
|
||||||
// if the status is a boost and the original post or another boost of it already exists < boostReinsertionDepth back in the timeline.
|
// if the status is a boost and the original post or another boost of it already exists < boostReinsertionDepth back in the timeline.
|
||||||
IndexAndPrepareOne(statusCreatedAt time.Time, statusID string) (bool, error)
|
IndexAndPrepareOne(statusCreatedAt time.Time, statusID string, boostOfID string, accountID string, boostOfAccountID string) (bool, error)
|
||||||
// OldestPreparedPostID returns the id of the rearmost (ie., the oldest) prepared post, or an error if something goes wrong.
|
// OldestPreparedPostID returns the id of the rearmost (ie., the oldest) prepared post, or an error if something goes wrong.
|
||||||
// If nothing goes wrong but there's no oldest post, an empty string will be returned so make sure to check for this.
|
// If nothing goes wrong but there's no oldest post, an empty string will be returned so make sure to check for this.
|
||||||
OldestPreparedPostID() (string, error)
|
OldestPreparedPostID() (string, error)
|
||||||
|
@ -109,6 +109,10 @@ type Timeline interface {
|
||||||
//
|
//
|
||||||
// The returned int indicates the amount of entries that were removed.
|
// The returned int indicates the amount of entries that were removed.
|
||||||
Remove(statusID string) (int, error)
|
Remove(statusID string) (int, error)
|
||||||
|
// RemoveAllBy removes all statuses by the given accountID, from both the index and prepared posts.
|
||||||
|
//
|
||||||
|
// The returned int indicates the amount of entries that were removed.
|
||||||
|
RemoveAllBy(accountID string) (int, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// timeline fulfils the Timeline interface
|
// timeline fulfils the Timeline interface
|
||||||
|
|
|
@ -111,6 +111,15 @@ type Likeable interface {
|
||||||
withObject
|
withObject
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Blockable represents the minimum interface for an activitystreams 'block' activity.
|
||||||
|
type Blockable interface {
|
||||||
|
withJSONLDId
|
||||||
|
withTypeName
|
||||||
|
|
||||||
|
withActor
|
||||||
|
withObject
|
||||||
|
}
|
||||||
|
|
||||||
// Announceable represents the minimum interface for an activitystreams 'announce' activity.
|
// Announceable represents the minimum interface for an activitystreams 'announce' activity.
|
||||||
type Announceable interface {
|
type Announceable interface {
|
||||||
withJSONLDId
|
withJSONLDId
|
||||||
|
|
|
@ -426,6 +426,41 @@ func (c *converter) ASLikeToFave(likeable Likeable) (*gtsmodel.StatusFave, error
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *converter) ASBlockToBlock(blockable Blockable) (*gtsmodel.Block, error) {
|
||||||
|
idProp := blockable.GetJSONLDId()
|
||||||
|
if idProp == nil || !idProp.IsIRI() {
|
||||||
|
return nil, errors.New("ASBlockToBlock: no id property set on block, or was not an iri")
|
||||||
|
}
|
||||||
|
uri := idProp.GetIRI().String()
|
||||||
|
|
||||||
|
origin, err := extractActor(blockable)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("ASBlockToBlock: error extracting actor property from block")
|
||||||
|
}
|
||||||
|
originAccount := >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) {
|
func (c *converter) ASAnnounceToStatus(announceable Announceable) (*gtsmodel.Status, bool, error) {
|
||||||
status := >smodel.Status{}
|
status := >smodel.Status{}
|
||||||
isNew := true
|
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.
|
// if something goes wrong. The returned account should be ready to serialize on an API level, and may NOT have sensitive fields.
|
||||||
// In other words, this is the public record that the server has of an account.
|
// In other words, this is the public record that the server has of an account.
|
||||||
AccountToMastoPublic(account *gtsmodel.Account) (*model.Account, error)
|
AccountToMastoPublic(account *gtsmodel.Account) (*model.Account, error)
|
||||||
|
// AccountToMastoBlocked takes a db model account as a param, and returns a mastotype account, or an error if
|
||||||
|
// something goes wrong. The returned account will be a bare minimum representation of the account. This function should be used
|
||||||
|
// when someone wants to view an account they've blocked.
|
||||||
|
AccountToMastoBlocked(account *gtsmodel.Account) (*model.Account, error)
|
||||||
// AppToMastoSensitive takes a db model application as a param, and returns a populated mastotype application, or an error
|
// AppToMastoSensitive takes a db model application as a param, and returns a populated mastotype application, or an error
|
||||||
// if something goes wrong. The returned application should be ready to serialize on an API level, and may have sensitive fields
|
// if something goes wrong. The returned application should be ready to serialize on an API level, and may have sensitive fields
|
||||||
// (such as client id and client secret), so serve it only to an authorized user who should have permission to see it.
|
// (such as client id and client secret), so serve it only to an authorized user who should have permission to see it.
|
||||||
|
@ -104,6 +108,8 @@ type TypeConverter interface {
|
||||||
ASFollowToFollow(followable Followable) (*gtsmodel.Follow, error)
|
ASFollowToFollow(followable Followable) (*gtsmodel.Follow, error)
|
||||||
// ASLikeToFave converts a remote activitystreams 'like' representation into a gts model status fave.
|
// ASLikeToFave converts a remote activitystreams 'like' representation into a gts model status fave.
|
||||||
ASLikeToFave(likeable Likeable) (*gtsmodel.StatusFave, error)
|
ASLikeToFave(likeable Likeable) (*gtsmodel.StatusFave, error)
|
||||||
|
// ASBlockToBlock converts a remote activity streams 'block' representation into a gts model block.
|
||||||
|
ASBlockToBlock(blockable Blockable) (*gtsmodel.Block, error)
|
||||||
// ASAnnounceToStatus converts an activitystreams 'announce' into a status.
|
// ASAnnounceToStatus converts an activitystreams 'announce' into a status.
|
||||||
//
|
//
|
||||||
// The returned bool indicates whether this status is new (true) or not new (false).
|
// The returned bool indicates whether this status is new (true) or not new (false).
|
||||||
|
@ -124,6 +130,11 @@ type TypeConverter interface {
|
||||||
|
|
||||||
// AccountToAS converts a gts model account into an activity streams person, suitable for federation
|
// AccountToAS converts a gts model account into an activity streams person, suitable for federation
|
||||||
AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error)
|
AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error)
|
||||||
|
// AccountToASMinimal converts a gts model account into an activity streams person, suitable for federation.
|
||||||
|
//
|
||||||
|
// The returned account will just have the Type, Username, PublicKey, and ID properties set. This is
|
||||||
|
// suitable for serving to requesters to whom we want to give as little information as possible because
|
||||||
|
// we don't trust them (yet).
|
||||||
AccountToASMinimal(a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error)
|
AccountToASMinimal(a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error)
|
||||||
// StatusToAS converts a gts model status into an activity streams note, suitable for federation
|
// StatusToAS converts a gts model status into an activity streams note, suitable for federation
|
||||||
StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, error)
|
StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, error)
|
||||||
|
@ -137,6 +148,8 @@ type TypeConverter interface {
|
||||||
FaveToAS(f *gtsmodel.StatusFave) (vocab.ActivityStreamsLike, error)
|
FaveToAS(f *gtsmodel.StatusFave) (vocab.ActivityStreamsLike, error)
|
||||||
// BoostToAS converts a gts model boost into an activityStreams ANNOUNCE, suitable for federation
|
// BoostToAS converts a gts model boost into an activityStreams ANNOUNCE, suitable for federation
|
||||||
BoostToAS(boostWrapperStatus *gtsmodel.Status, boostingAccount *gtsmodel.Account, boostedAccount *gtsmodel.Account) (vocab.ActivityStreamsAnnounce, error)
|
BoostToAS(boostWrapperStatus *gtsmodel.Status, boostingAccount *gtsmodel.Account, boostedAccount *gtsmodel.Account) (vocab.ActivityStreamsAnnounce, error)
|
||||||
|
// BlockToAS converts a gts model block into an activityStreams BLOCK, suitable for federation.
|
||||||
|
BlockToAS(block *gtsmodel.Block) (vocab.ActivityStreamsBlock, error)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
INTERNAL (gts) MODEL TO INTERNAL MODEL
|
INTERNAL (gts) MODEL TO INTERNAL MODEL
|
||||||
|
|
|
@ -67,6 +67,7 @@ func (c *converter) StatusToBoost(s *gtsmodel.Status, boostingAccount *gtsmodel.
|
||||||
Language: s.Language,
|
Language: s.Language,
|
||||||
Text: s.Text,
|
Text: s.Text,
|
||||||
BoostOfID: s.ID,
|
BoostOfID: s.ID,
|
||||||
|
BoostOfAccountID: s.AccountID,
|
||||||
Visibility: s.Visibility,
|
Visibility: s.Visibility,
|
||||||
VisibilityAdvanced: s.VisibilityAdvanced,
|
VisibilityAdvanced: s.VisibilityAdvanced,
|
||||||
|
|
||||||
|
|
|
@ -780,3 +780,73 @@ func (c *converter) BoostToAS(boostWrapperStatus *gtsmodel.Status, boostingAccou
|
||||||
|
|
||||||
return announce, nil
|
return announce, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
we want to end up with something like this:
|
||||||
|
|
||||||
|
{
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
"actor": "https://example.org/users/some_user",
|
||||||
|
"id":"https://example.org/users/some_user/blocks/SOME_ULID_OF_A_BLOCK",
|
||||||
|
"object":"https://some_other.instance/users/some_other_user",
|
||||||
|
"type":"Block"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
func (c *converter) BlockToAS(b *gtsmodel.Block) (vocab.ActivityStreamsBlock, error) {
|
||||||
|
if b.Account == nil {
|
||||||
|
a := >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
|
acct = a.Username
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var suspended bool
|
||||||
|
if !a.SuspendedAt.IsZero() {
|
||||||
|
suspended = true
|
||||||
|
}
|
||||||
|
|
||||||
return &model.Account{
|
return &model.Account{
|
||||||
ID: a.ID,
|
ID: a.ID,
|
||||||
Username: a.Username,
|
Username: a.Username,
|
||||||
|
@ -170,6 +175,34 @@ func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*model.Account, e
|
||||||
LastStatusAt: lastStatusAt,
|
LastStatusAt: lastStatusAt,
|
||||||
Emojis: emojis, // TODO: implement this
|
Emojis: emojis, // TODO: implement this
|
||||||
Fields: fields,
|
Fields: fields,
|
||||||
|
Suspended: suspended,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *converter) AccountToMastoBlocked(a *gtsmodel.Account) (*model.Account, error) {
|
||||||
|
var acct string
|
||||||
|
if a.Domain != "" {
|
||||||
|
// this is a remote user
|
||||||
|
acct = fmt.Sprintf("%s@%s", a.Username, a.Domain)
|
||||||
|
} else {
|
||||||
|
// this is a local user
|
||||||
|
acct = a.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
var suspended bool
|
||||||
|
if !a.SuspendedAt.IsZero() {
|
||||||
|
suspended = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.Account{
|
||||||
|
ID: a.ID,
|
||||||
|
Username: a.Username,
|
||||||
|
Acct: acct,
|
||||||
|
DisplayName: a.DisplayName,
|
||||||
|
Bot: a.Bot,
|
||||||
|
CreatedAt: a.CreatedAt.Format(time.RFC3339),
|
||||||
|
URL: a.URL,
|
||||||
|
Suspended: suspended,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -104,4 +104,9 @@ var (
|
||||||
// from eg /users/example_username/statuses/01F7XT5JZW1WMVSW1KADS8PVDH
|
// from eg /users/example_username/statuses/01F7XT5JZW1WMVSW1KADS8PVDH
|
||||||
// The regex can be played with here: https://regex101.com/r/G9zuxQ/1
|
// The regex can be played with here: https://regex101.com/r/G9zuxQ/1
|
||||||
statusesPathRegex = regexp.MustCompile(statusesPathRegexString)
|
statusesPathRegex = regexp.MustCompile(statusesPathRegexString)
|
||||||
|
|
||||||
|
blockPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, UsersPath, usernameRegexString, BlocksPath, ulidRegexString)
|
||||||
|
// blockPathRegex parses a path that validates and captures the username part and the ulid part
|
||||||
|
// from eg /users/example_username/blocks/01F7XT5JZW1WMVSW1KADS8PVDH
|
||||||
|
blockPathRegex = regexp.MustCompile(blockPathRegexString)
|
||||||
)
|
)
|
||||||
|
|
|
@ -50,6 +50,8 @@ const (
|
||||||
FollowPath = "follow"
|
FollowPath = "follow"
|
||||||
// UpdatePath is used to generate the URI for an account update
|
// UpdatePath is used to generate the URI for an account update
|
||||||
UpdatePath = "updates"
|
UpdatePath = "updates"
|
||||||
|
// BlocksPath is used to generate the URI for a block
|
||||||
|
BlocksPath = "blocks"
|
||||||
)
|
)
|
||||||
|
|
||||||
// APContextKey is a type used specifically for settings values on contexts within go-fed AP request chains
|
// APContextKey is a type used specifically for settings values on contexts within go-fed AP request chains
|
||||||
|
@ -124,6 +126,12 @@ func GenerateURIForUpdate(username string, protocol string, host string, thisUpd
|
||||||
return fmt.Sprintf("%s://%s/%s/%s#%s/%s", protocol, host, UsersPath, username, UpdatePath, thisUpdateID)
|
return fmt.Sprintf("%s://%s/%s/%s#%s/%s", protocol, host, UsersPath, username, UpdatePath, thisUpdateID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateURIForBlock returns the AP URI for a new block activity -- something like:
|
||||||
|
// https://example.org/users/whatever_user/blocks/01F7XTH1QGBAPMGF49WJZ91XGC
|
||||||
|
func GenerateURIForBlock(username string, protocol string, host string, thisBlockID string) string {
|
||||||
|
return fmt.Sprintf("%s://%s/%s/%s/%s/%s", protocol, host, UsersPath, username, BlocksPath, thisBlockID)
|
||||||
|
}
|
||||||
|
|
||||||
// GenerateURIsForAccount throws together a bunch of URIs for the given username, with the given protocol and host.
|
// GenerateURIsForAccount throws together a bunch of URIs for the given username, with the given protocol and host.
|
||||||
func GenerateURIsForAccount(username string, protocol string, host string) *UserURIs {
|
func GenerateURIsForAccount(username string, protocol string, host string) *UserURIs {
|
||||||
// The below URLs are used for serving web requests
|
// The below URLs are used for serving web requests
|
||||||
|
@ -214,6 +222,11 @@ func IsPublicKeyPath(id *url.URL) bool {
|
||||||
return userPublicKeyPathRegex.MatchString(id.Path)
|
return userPublicKeyPathRegex.MatchString(id.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsBlockPath returns true if the given URL path corresponds to eg /users/example_username/blocks/SOME_ULID_OF_A_BLOCK
|
||||||
|
func IsBlockPath(id *url.URL) bool {
|
||||||
|
return blockPathRegex.MatchString(id.Path)
|
||||||
|
}
|
||||||
|
|
||||||
// ParseStatusesPath returns the username and ulid from a path such as /users/example_username/statuses/SOME_ULID_OF_A_STATUS
|
// ParseStatusesPath returns the username and ulid from a path such as /users/example_username/statuses/SOME_ULID_OF_A_STATUS
|
||||||
func ParseStatusesPath(id *url.URL) (username string, ulid string, err error) {
|
func ParseStatusesPath(id *url.URL) (username string, ulid string, err error) {
|
||||||
matches := statusesPathRegex.FindStringSubmatch(id.Path)
|
matches := statusesPathRegex.FindStringSubmatch(id.Path)
|
||||||
|
@ -292,3 +305,15 @@ func ParseLikedPath(id *url.URL) (username string, ulid string, err error) {
|
||||||
ulid = matches[2]
|
ulid = matches[2]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseBlockPath returns the username and ulid from a path such as /users/example_username/blocks/SOME_ULID_OF_A_BLOCK
|
||||||
|
func ParseBlockPath(id *url.URL) (username string, ulid string, err error) {
|
||||||
|
matches := blockPathRegex.FindStringSubmatch(id.Path)
|
||||||
|
if len(matches) != 3 {
|
||||||
|
err = fmt.Errorf("expected 3 matches but matches length was %d", len(matches))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
username = matches[1]
|
||||||
|
ulid = matches[2]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue