mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2024-11-26 22:30:25 +00:00
Follows and relationships (#27)
* Follows -- create and undo, both remote and local * Statuses -- federate new posts, including media, attachments, CWs and image descriptions.
This commit is contained in:
parent
dc06e71b76
commit
d839f27c30
54 changed files with 2260 additions and 299 deletions
|
@ -17,12 +17,12 @@
|
||||||
* [x] /api/v1/accounts/:id GET (Get account information)
|
* [x] /api/v1/accounts/:id GET (Get account information)
|
||||||
* [x] /api/v1/accounts/:id/statuses GET (Get an account's statuses)
|
* [x] /api/v1/accounts/:id/statuses GET (Get an account's statuses)
|
||||||
* [x] /api/v1/accounts/:id/followers GET (Get an account's followers)
|
* [x] /api/v1/accounts/:id/followers GET (Get an account's followers)
|
||||||
* [ ] /api/v1/accounts/:id/following GET (Get an account's following)
|
* [x] /api/v1/accounts/:id/following GET (Get an account's following)
|
||||||
* [ ] /api/v1/accounts/:id/featured_tags GET (Get an account's featured tags)
|
* [ ] /api/v1/accounts/:id/featured_tags GET (Get an account's featured tags)
|
||||||
* [ ] /api/v1/accounts/:id/lists GET (Get lists containing this account)
|
* [ ] /api/v1/accounts/:id/lists GET (Get lists containing this account)
|
||||||
* [ ] /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)
|
||||||
* [ ] /api/v1/accounts/:id/follow POST (Follow this account)
|
* [x] /api/v1/accounts/:id/follow POST (Follow this account)
|
||||||
* [ ] /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)
|
* [ ] /api/v1/accounts/:id/block POST (Block this account)
|
||||||
* [ ] /api/v1/accounts/:id/unblock POST (Unblock this account)
|
* [ ] /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)
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
* [ ] /api/v1/accounts/:id/pin POST (Feature this account on profile)
|
* [ ] /api/v1/accounts/:id/pin POST (Feature this account on profile)
|
||||||
* [ ] /api/v1/accounts/:id/unpin POST (Remove this account from profile)
|
* [ ] /api/v1/accounts/:id/unpin POST (Remove this account from profile)
|
||||||
* [ ] /api/v1/accounts/:id/note POST (Make a personal note about this account)
|
* [ ] /api/v1/accounts/:id/note POST (Make a personal note about this account)
|
||||||
* [ ] /api/v1/accounts/relationships GET (Check relationships with accounts)
|
* [x] /api/v1/accounts/relationships GET (Check relationships with accounts)
|
||||||
* [ ] /api/v1/accounts/search GET (Search for an account)
|
* [ ] /api/v1/accounts/search GET (Search for an account)
|
||||||
* [ ] Bookmarks
|
* [ ] Bookmarks
|
||||||
* [ ] /api/v1/bookmarks GET (See bookmarked statuses)
|
* [ ] /api/v1/bookmarks GET (See bookmarked statuses)
|
||||||
|
@ -177,6 +177,7 @@
|
||||||
* [ ] 'Greedy' federation
|
* [ ] 'Greedy' federation
|
||||||
* [ ] No federation (insulate this instance from the Fediverse)
|
* [ ] No federation (insulate this instance from the Fediverse)
|
||||||
* [ ] Allowlist
|
* [ ] Allowlist
|
||||||
|
* [x] Secure HTTP signatures (creation and validation)
|
||||||
* [ ] Storage
|
* [ ] Storage
|
||||||
* [x] Internal/statuses/preferences etc
|
* [x] Internal/statuses/preferences etc
|
||||||
* [x] Postgres interface
|
* [x] Postgres interface
|
||||||
|
|
|
@ -57,6 +57,14 @@ const (
|
||||||
GetStatusesPath = BasePathWithID + "/statuses"
|
GetStatusesPath = BasePathWithID + "/statuses"
|
||||||
// GetFollowersPath is for showing an account's followers
|
// GetFollowersPath is for showing an account's followers
|
||||||
GetFollowersPath = BasePathWithID + "/followers"
|
GetFollowersPath = BasePathWithID + "/followers"
|
||||||
|
// GetFollowingPath is for showing account's that an account follows.
|
||||||
|
GetFollowingPath = BasePathWithID + "/following"
|
||||||
|
// GetRelationshipsPath is for showing an account's relationship with other accounts
|
||||||
|
GetRelationshipsPath = BasePath + "/relationships"
|
||||||
|
// FollowPath is for POSTing new follows to, and updating existing follows
|
||||||
|
PostFollowPath = BasePathWithID + "/follow"
|
||||||
|
// PostUnfollowPath is for POSTing an unfollow
|
||||||
|
PostUnfollowPath = BasePathWithID + "/unfollow"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Module implements the ClientAPIModule interface for account-related actions
|
// Module implements the ClientAPIModule interface for account-related actions
|
||||||
|
@ -82,6 +90,10 @@ func (m *Module) Route(r router.Router) error {
|
||||||
r.AttachHandler(http.MethodPatch, BasePathWithID, m.muxHandler)
|
r.AttachHandler(http.MethodPatch, BasePathWithID, m.muxHandler)
|
||||||
r.AttachHandler(http.MethodGet, GetStatusesPath, m.AccountStatusesGETHandler)
|
r.AttachHandler(http.MethodGet, GetStatusesPath, m.AccountStatusesGETHandler)
|
||||||
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, GetRelationshipsPath, m.AccountRelationshipsGETHandler)
|
||||||
|
r.AttachHandler(http.MethodPost, PostFollowPath, m.AccountFollowPOSTHandler)
|
||||||
|
r.AttachHandler(http.MethodPost, PostUnfollowPath, m.AccountUnfollowPOSTHandler)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
56
internal/api/client/account/follow.go
Normal file
56
internal/api/client/account/follow.go
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
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/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AccountFollowPOSTHandler is the endpoint for creating a new follow request to the target account
|
||||||
|
func (m *Module) AccountFollowPOSTHandler(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
|
||||||
|
}
|
||||||
|
form := &model.AccountFollowRequest{}
|
||||||
|
if err := c.ShouldBind(form); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
form.TargetAccountID = targetAcctID
|
||||||
|
|
||||||
|
relationship, errWithCode := m.processor.AccountFollowCreate(authed, form)
|
||||||
|
if errWithCode != nil {
|
||||||
|
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, relationship)
|
||||||
|
}
|
49
internal/api/client/account/following.go
Normal file
49
internal/api/client/account/following.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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AccountFollowersGETHandler serves the followers of the requested account, if they're visible to the requester.
|
||||||
|
func (m *Module) AccountFollowingGETHandler(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
|
||||||
|
}
|
||||||
|
|
||||||
|
following, errWithCode := m.processor.AccountFollowingGet(authed, targetAcctID)
|
||||||
|
if errWithCode != nil {
|
||||||
|
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, following)
|
||||||
|
}
|
46
internal/api/client/account/relationships.go
Normal file
46
internal/api/client/account/relationships.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package account
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AccountRelationshipsGETHandler serves the relationship of the requesting account with one or more requested account IDs.
|
||||||
|
func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) {
|
||||||
|
l := m.log.WithField("func", "AccountRelationshipsGETHandler")
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
targetAccountIDs := c.QueryArray("id[]")
|
||||||
|
if len(targetAccountIDs) == 0 {
|
||||||
|
// check fallback -- let's be generous and see if maybe it's just set as 'id'?
|
||||||
|
id := c.Query("id")
|
||||||
|
if id == "" {
|
||||||
|
l.Debug("no account id specified in query")
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
targetAccountIDs = append(targetAccountIDs, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
relationships := []model.Relationship{}
|
||||||
|
|
||||||
|
for _, targetAccountID := range targetAccountIDs {
|
||||||
|
r, errWithCode := m.processor.AccountRelationshipGet(authed, targetAccountID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
relationships = append(relationships, *r)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, relationships)
|
||||||
|
}
|
53
internal/api/client/account/unfollow.go
Normal file
53
internal/api/client/account/unfollow.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AccountUnfollowPOSTHandler is the endpoint for removing a follow and/or follow request to the target account
|
||||||
|
func (m *Module) AccountUnfollowPOSTHandler(c *gin.Context) {
|
||||||
|
l := m.log.WithField("func", "AccountUnfollowPOSTHandler")
|
||||||
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
|
if err != nil {
|
||||||
|
l.Debug(err)
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
targetAcctID := c.Param(IDKey)
|
||||||
|
if targetAcctID == "" {
|
||||||
|
l.Debug(err)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
relationship, errWithCode := m.processor.AccountFollowRemove(authed, targetAcctID)
|
||||||
|
if errWithCode != nil {
|
||||||
|
l.Debug(errWithCode.Error())
|
||||||
|
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, relationship)
|
||||||
|
}
|
|
@ -28,6 +28,7 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -60,7 +61,7 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) {
|
||||||
app := >smodel.Application{
|
app := >smodel.Application{
|
||||||
ClientID: clientID,
|
ClientID: clientID,
|
||||||
}
|
}
|
||||||
if err := m.db.GetWhere("client_id", app.ClientID, app); err != nil {
|
if err := m.db.GetWhere([]db.Where{{Key: "client_id", Value: app.ClientID}}, app); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("no application found for client id %s", clientID)})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("no application found for client id %s", clientID)})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
@ -68,7 +69,7 @@ func (m *Module) OauthTokenMiddleware(c *gin.Context) {
|
||||||
if cid := ti.GetClientID(); cid != "" {
|
if cid := ti.GetClientID(); cid != "" {
|
||||||
l.Tracef("authenticated client %s with bearer token, scope is %s", cid, ti.GetScope())
|
l.Tracef("authenticated client %s with bearer token, scope is %s", cid, ti.GetScope())
|
||||||
app := >smodel.Application{}
|
app := >smodel.Application{}
|
||||||
if err := m.db.GetWhere("client_id", cid, app); err != nil {
|
if err := m.db.GetWhere([]db.Where{{Key: "client_id",Value: cid}}, app); err != nil {
|
||||||
l.Tracef("no app found for client %s", cid)
|
l.Tracef("no app found for client %s", cid)
|
||||||
}
|
}
|
||||||
c.Set(oauth.SessionAuthorizedApplication, app)
|
c.Set(oauth.SessionAuthorizedApplication, app)
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
|
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
@ -87,7 +88,7 @@ func (m *Module) ValidatePassword(email string, password string) (userid string,
|
||||||
// first we select the user from the database based on email address, bail if no user found for that email
|
// first we select the user from the database based on email address, bail if no user found for that email
|
||||||
gtsUser := >smodel.User{}
|
gtsUser := >smodel.User{}
|
||||||
|
|
||||||
if err := m.db.GetWhere("email", email, gtsUser); err != nil {
|
if err := m.db.GetWhere([]db.Where{{Key: "email", Value: email}}, gtsUser); err != nil {
|
||||||
l.Debugf("user %s was not retrievable from db during oauth authorization attempt: %s", email, err)
|
l.Debugf("user %s was not retrievable from db during oauth authorization attempt: %s", email, err)
|
||||||
return incorrectPassword()
|
return incorrectPassword()
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,10 +48,11 @@ func (m *Module) FollowRequestAcceptPOSTHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if errWithCode := m.processor.FollowRequestAccept(authed, originAccountID); errWithCode != nil {
|
r, errWithCode := m.processor.FollowRequestAccept(authed, originAccountID)
|
||||||
|
if errWithCode != nil {
|
||||||
l.Debug(errWithCode.Error())
|
l.Debug(errWithCode.Error())
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Status(http.StatusOK)
|
c.JSON(http.StatusOK, r)
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ import (
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/status"
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/status"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
|
@ -118,7 +119,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatus() {
|
||||||
}, statusReply.Tags[0])
|
}, statusReply.Tags[0])
|
||||||
|
|
||||||
gtsTag := >smodel.Tag{}
|
gtsTag := >smodel.Tag{}
|
||||||
err = suite.db.GetWhere("name", "helloworld", gtsTag)
|
err = suite.db.GetWhere([]db.Where{{Key: "name", Value: "helloworld"}}, gtsTag)
|
||||||
assert.NoError(suite.T(), err)
|
assert.NoError(suite.T(), err)
|
||||||
assert.Equal(suite.T(), statusReply.Account.ID, gtsTag.FirstSeenFromAccountID)
|
assert.Equal(suite.T(), statusReply.Account.ID, gtsTag.FirstSeenFromAccountID)
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,18 +77,18 @@ type Account struct {
|
||||||
// See https://docs.joinmastodon.org/methods/accounts/
|
// See https://docs.joinmastodon.org/methods/accounts/
|
||||||
type AccountCreateRequest struct {
|
type AccountCreateRequest struct {
|
||||||
// Text that will be reviewed by moderators if registrations require manual approval.
|
// Text that will be reviewed by moderators if registrations require manual approval.
|
||||||
Reason string `form:"reason"`
|
Reason string `form:"reason" json:"reason" xml:"reason"`
|
||||||
// The desired username for the account
|
// The desired username for the account
|
||||||
Username string `form:"username" binding:"required"`
|
Username string `form:"username" json:"username" xml:"username" binding:"required"`
|
||||||
// The email address to be used for login
|
// The email address to be used for login
|
||||||
Email string `form:"email" binding:"required"`
|
Email string `form:"email" json:"email" xml:"email" binding:"required"`
|
||||||
// The password to be used for login
|
// The password to be used for login
|
||||||
Password string `form:"password" binding:"required"`
|
Password string `form:"password" json:"password" xml:"password" binding:"required"`
|
||||||
// Whether the user agrees to the local rules, terms, and policies.
|
// Whether the user agrees to the local rules, terms, and policies.
|
||||||
// These should be presented to the user in order to allow them to consent before setting this parameter to TRUE.
|
// These should be presented to the user in order to allow them to consent before setting this parameter to TRUE.
|
||||||
Agreement bool `form:"agreement" binding:"required"`
|
Agreement bool `form:"agreement" json:"agreement" xml:"agreement" binding:"required"`
|
||||||
// The language of the confirmation email that will be sent
|
// The language of the confirmation email that will be sent
|
||||||
Locale string `form:"locale" binding:"required"`
|
Locale string `form:"locale" json:"locale" xml:"locale" binding:"required"`
|
||||||
// The IP of the sign up request, will not be parsed from the form but must be added manually
|
// The IP of the sign up request, will not be parsed from the form but must be added manually
|
||||||
IP net.IP `form:"-"`
|
IP net.IP `form:"-"`
|
||||||
}
|
}
|
||||||
|
@ -97,40 +97,51 @@ type AccountCreateRequest struct {
|
||||||
// See https://docs.joinmastodon.org/methods/accounts/
|
// See https://docs.joinmastodon.org/methods/accounts/
|
||||||
type UpdateCredentialsRequest struct {
|
type UpdateCredentialsRequest struct {
|
||||||
// Whether the account should be shown in the profile directory.
|
// Whether the account should be shown in the profile directory.
|
||||||
Discoverable *bool `form:"discoverable"`
|
Discoverable *bool `form:"discoverable" json:"discoverable" xml:"discoverable"`
|
||||||
// Whether the account has a bot flag.
|
// Whether the account has a bot flag.
|
||||||
Bot *bool `form:"bot"`
|
Bot *bool `form:"bot" json:"bot" xml:"bot"`
|
||||||
// The display name to use for the profile.
|
// The display name to use for the profile.
|
||||||
DisplayName *string `form:"display_name"`
|
DisplayName *string `form:"display_name" json:"display_name" xml:"display_name"`
|
||||||
// The account bio.
|
// The account bio.
|
||||||
Note *string `form:"note"`
|
Note *string `form:"note" json:"note" xml:"note"`
|
||||||
// Avatar image encoded using multipart/form-data
|
// Avatar image encoded using multipart/form-data
|
||||||
Avatar *multipart.FileHeader `form:"avatar"`
|
Avatar *multipart.FileHeader `form:"avatar" json:"avatar" xml:"avatar"`
|
||||||
// Header image encoded using multipart/form-data
|
// Header image encoded using multipart/form-data
|
||||||
Header *multipart.FileHeader `form:"header"`
|
Header *multipart.FileHeader `form:"header" json:"header" xml:"header"`
|
||||||
// Whether manual approval of follow requests is required.
|
// Whether manual approval of follow requests is required.
|
||||||
Locked *bool `form:"locked"`
|
Locked *bool `form:"locked" json:"locked" xml:"locked"`
|
||||||
// New Source values for this account
|
// New Source values for this account
|
||||||
Source *UpdateSource `form:"source"`
|
Source *UpdateSource `form:"source" json:"source" xml:"source"`
|
||||||
// Profile metadata name and value
|
// Profile metadata name and value
|
||||||
FieldsAttributes *[]UpdateField `form:"fields_attributes"`
|
FieldsAttributes *[]UpdateField `form:"fields_attributes" json:"fields_attributes" xml:"fields_attributes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateSource is to be used specifically in an UpdateCredentialsRequest.
|
// UpdateSource is to be used specifically in an UpdateCredentialsRequest.
|
||||||
type UpdateSource struct {
|
type UpdateSource struct {
|
||||||
// Default post privacy for authored statuses.
|
// Default post privacy for authored statuses.
|
||||||
Privacy *string `form:"privacy"`
|
Privacy *string `form:"privacy" json:"privacy" xml:"privacy"`
|
||||||
// Whether to mark authored statuses as sensitive by default.
|
// Whether to mark authored statuses as sensitive by default.
|
||||||
Sensitive *bool `form:"sensitive"`
|
Sensitive *bool `form:"sensitive" json:"sensitive" xml:"sensitive"`
|
||||||
// Default language to use for authored statuses. (ISO 6391)
|
// Default language to use for authored statuses. (ISO 6391)
|
||||||
Language *string `form:"language"`
|
Language *string `form:"language" json:"language" xml:"language"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateField is to be used specifically in an UpdateCredentialsRequest.
|
// UpdateField is to be used specifically in an UpdateCredentialsRequest.
|
||||||
// By default, max 4 fields and 255 characters per property/value.
|
// By default, max 4 fields and 255 characters per property/value.
|
||||||
type UpdateField struct {
|
type UpdateField struct {
|
||||||
// Name of the field
|
// Name of the field
|
||||||
Name *string `form:"name"`
|
Name *string `form:"name" json:"name" xml:"name"`
|
||||||
// Value of the field
|
// Value of the field
|
||||||
Value *string `form:"value"`
|
Value *string `form:"value" json:"value" xml:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountFollowRequest is for parsing requests at /api/v1/accounts/:id/follow
|
||||||
|
type AccountFollowRequest struct {
|
||||||
|
// ID of the account to follow request
|
||||||
|
// This should be a URL parameter not a form field
|
||||||
|
TargetAccountID string `form:"-"`
|
||||||
|
// Show reblogs for this account?
|
||||||
|
Reblogs *bool `form:"reblogs" json:"reblogs" xml:"reblogs"`
|
||||||
|
// Notify when this account posts?
|
||||||
|
Notify *bool `form:"notify" json:"notify" xml:"notify"`
|
||||||
}
|
}
|
||||||
|
|
58
internal/api/s2s/user/followers.go
Normal file
58
internal/api/s2s/user/followers.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
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 user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Module) FollowersGETHandler(c *gin.Context) {
|
||||||
|
l := m.log.WithFields(logrus.Fields{
|
||||||
|
"func": "FollowersGETHandler",
|
||||||
|
"url": c.Request.RequestURI,
|
||||||
|
})
|
||||||
|
|
||||||
|
requestedUsername := c.Param(UsernameKey)
|
||||||
|
if requestedUsername == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure this actually an AP request
|
||||||
|
format := c.NegotiateFormat(ActivityPubAcceptHeaders...)
|
||||||
|
if format == "" {
|
||||||
|
c.JSON(http.StatusNotAcceptable, gin.H{"error": "could not negotiate format with given Accept header(s)"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.Tracef("negotiated format: %s", format)
|
||||||
|
|
||||||
|
// make a copy of the context to pass along so we don't break anything
|
||||||
|
cp := c.Copy()
|
||||||
|
user, err := m.processor.GetFediFollowers(requestedUsername, cp.Request) // GetFediUser handles auth as well
|
||||||
|
if err != nil {
|
||||||
|
l.Info(err.Error())
|
||||||
|
c.JSON(err.Code(), gin.H{"error": err.Safe()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, user)
|
||||||
|
}
|
46
internal/api/s2s/user/statusget.go
Normal file
46
internal/api/s2s/user/statusget.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Module) StatusGETHandler(c *gin.Context) {
|
||||||
|
l := m.log.WithFields(logrus.Fields{
|
||||||
|
"func": "StatusGETHandler",
|
||||||
|
"url": c.Request.RequestURI,
|
||||||
|
})
|
||||||
|
|
||||||
|
requestedUsername := c.Param(UsernameKey)
|
||||||
|
if requestedUsername == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requestedStatusID := c.Param(StatusIDKey)
|
||||||
|
if requestedStatusID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id specified in request"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure this actually an AP request
|
||||||
|
format := c.NegotiateFormat(ActivityPubAcceptHeaders...)
|
||||||
|
if format == "" {
|
||||||
|
c.JSON(http.StatusNotAcceptable, gin.H{"error": "could not negotiate format with given Accept header(s)"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.Tracef("negotiated format: %s", format)
|
||||||
|
|
||||||
|
// make a copy of the context to pass along so we don't break anything
|
||||||
|
cp := c.Copy()
|
||||||
|
status, err := m.processor.GetFediStatus(requestedUsername, requestedStatusID, cp.Request) // handles auth as well
|
||||||
|
if err != nil {
|
||||||
|
l.Info(err.Error())
|
||||||
|
c.JSON(err.Code(), gin.H{"error": err.Safe()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, status)
|
||||||
|
}
|
|
@ -32,6 +32,8 @@ import (
|
||||||
const (
|
const (
|
||||||
// UsernameKey is for account usernames.
|
// UsernameKey is for account usernames.
|
||||||
UsernameKey = "username"
|
UsernameKey = "username"
|
||||||
|
// StatusIDKey is for status IDs
|
||||||
|
StatusIDKey = "status"
|
||||||
// UsersBasePath is the base path for serving information about Users eg https://example.org/users
|
// UsersBasePath is the base path for serving information about Users eg https://example.org/users
|
||||||
UsersBasePath = "/" + util.UsersPath
|
UsersBasePath = "/" + util.UsersPath
|
||||||
// UsersBasePathWithUsername is just the users base path with the Username key in it.
|
// UsersBasePathWithUsername is just the users base path with the Username key in it.
|
||||||
|
@ -40,6 +42,10 @@ const (
|
||||||
UsersBasePathWithUsername = UsersBasePath + "/:" + UsernameKey
|
UsersBasePathWithUsername = UsersBasePath + "/:" + UsernameKey
|
||||||
// UsersInboxPath is for serving POST requests to a user's inbox with the given username key.
|
// UsersInboxPath is for serving POST requests to a user's inbox with the given username key.
|
||||||
UsersInboxPath = UsersBasePathWithUsername + "/" + util.InboxPath
|
UsersInboxPath = UsersBasePathWithUsername + "/" + util.InboxPath
|
||||||
|
// UsersFollowersPath is for serving GET request's to a user's followers list, with the given username key.
|
||||||
|
UsersFollowersPath = UsersBasePathWithUsername + "/" + util.FollowersPath
|
||||||
|
// UsersStatusPath is for serving GET requests to a particular status by a user, with the given username key and status ID
|
||||||
|
UsersStatusPath = UsersBasePathWithUsername + "/" + util.StatusesPath + "/:" + StatusIDKey
|
||||||
)
|
)
|
||||||
|
|
||||||
// ActivityPubAcceptHeaders represents the Accept headers mentioned here:
|
// ActivityPubAcceptHeaders represents the Accept headers mentioned here:
|
||||||
|
@ -69,5 +75,7 @@ func New(config *config.Config, processor message.Processor, log *logrus.Logger)
|
||||||
func (m *Module) Route(s router.Router) error {
|
func (m *Module) Route(s router.Router) error {
|
||||||
s.AttachHandler(http.MethodGet, UsersBasePathWithUsername, m.UsersGETHandler)
|
s.AttachHandler(http.MethodGet, UsersBasePathWithUsername, m.UsersGETHandler)
|
||||||
s.AttachHandler(http.MethodPost, UsersInboxPath, m.InboxPOSTHandler)
|
s.AttachHandler(http.MethodPost, UsersInboxPath, m.InboxPOSTHandler)
|
||||||
|
s.AttachHandler(http.MethodGet, UsersFollowersPath, m.FollowersGETHandler)
|
||||||
|
s.AttachHandler(http.MethodGet, UsersStatusPath, m.StatusGETHandler)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,5 +43,6 @@ func New(config *config.Config, log *logrus.Logger) api.ClientModule {
|
||||||
func (m *Module) Route(s router.Router) error {
|
func (m *Module) Route(s router.Router) error {
|
||||||
s.AttachMiddleware(m.FlocBlock)
|
s.AttachMiddleware(m.FlocBlock)
|
||||||
s.AttachMiddleware(m.ExtraHeaders)
|
s.AttachMiddleware(m.ExtraHeaders)
|
||||||
|
s.AttachMiddleware(m.UserAgentBlock)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
43
internal/api/security/useragentblock.go
Normal file
43
internal/api/security/useragentblock.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
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 security
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserAgentBlock is a middleware that prevents google chrome cohort tracking by
|
||||||
|
// writing the Permissions-Policy header after all other parts of the request have been completed.
|
||||||
|
// See: https://plausible.io/blog/google-floc
|
||||||
|
func (m *Module) UserAgentBlock(c *gin.Context) {
|
||||||
|
|
||||||
|
ua := c.Request.UserAgent()
|
||||||
|
if ua == "" {
|
||||||
|
c.AbortWithStatus(http.StatusTeapot)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(strings.ToLower(c.Request.UserAgent()), strings.ToLower("friendica")) {
|
||||||
|
c.AbortWithStatus(http.StatusTeapot)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,7 +22,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/go-fed/activity/pub"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -33,18 +32,28 @@ const (
|
||||||
|
|
||||||
// ErrNoEntries is to be returned from the DB interface when no entries are found for a given query.
|
// ErrNoEntries is to be returned from the DB interface when no entries are found for a given query.
|
||||||
type ErrNoEntries struct{}
|
type ErrNoEntries struct{}
|
||||||
|
|
||||||
func (e ErrNoEntries) Error() string {
|
func (e ErrNoEntries) Error() string {
|
||||||
return "no entries"
|
return "no entries"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrAlreadyExists is to be returned from the DB interface when an entry already exists for a given query or its constraints.
|
||||||
|
type ErrAlreadyExists struct{}
|
||||||
|
func (e ErrAlreadyExists) Error() string {
|
||||||
|
return "already exists"
|
||||||
|
}
|
||||||
|
|
||||||
|
type Where struct {
|
||||||
|
Key string
|
||||||
|
Value interface{}
|
||||||
|
}
|
||||||
|
|
||||||
// DB provides methods for interacting with an underlying database or other storage mechanism (for now, just postgres).
|
// DB provides methods for interacting with an underlying database or other storage mechanism (for now, just postgres).
|
||||||
// Note that in all of the functions below, the passed interface should be a pointer or a slice, which will then be populated
|
// Note that in all of the functions below, the passed interface should be a pointer or a slice, which will then be populated
|
||||||
// by whatever is returned from the database.
|
// by whatever is returned from the database.
|
||||||
type DB interface {
|
type DB interface {
|
||||||
// Federation returns an interface that's compatible with go-fed, for performing federation storage/retrieval functions.
|
// Federation returns an interface that's compatible with go-fed, for performing federation storage/retrieval functions.
|
||||||
// See: https://pkg.go.dev/github.com/go-fed/activity@v1.0.0/pub?utm_source=gopls#Database
|
// See: https://pkg.go.dev/github.com/go-fed/activity@v1.0.0/pub?utm_source=gopls#Database
|
||||||
Federation() pub.Database
|
// Federation() federatingdb.FederatingDB
|
||||||
|
|
||||||
/*
|
/*
|
||||||
BASIC DB FUNCTIONALITY
|
BASIC DB FUNCTIONALITY
|
||||||
|
@ -75,7 +84,7 @@ type DB interface {
|
||||||
// name of the key to select from.
|
// name of the key to select from.
|
||||||
// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice.
|
// The given interface i will be set to the result of the query, whatever it is. Use a pointer or a slice.
|
||||||
// In case of no entries, a 'no entries' error will be returned
|
// In case of no entries, a 'no entries' error will be returned
|
||||||
GetWhere(key string, value interface{}, i interface{}) error
|
GetWhere(where []Where, i interface{}) error
|
||||||
|
|
||||||
// // GetWhereMany gets one entry where key = value for *ALL* parameters passed as "where".
|
// // GetWhereMany gets one entry where key = value for *ALL* parameters passed as "where".
|
||||||
// // That is, if you pass 2 'where' entries, with 1 being Key username and Value test, and the second
|
// // That is, if you pass 2 'where' entries, with 1 being Key username and Value test, and the second
|
||||||
|
@ -109,7 +118,7 @@ type DB interface {
|
||||||
|
|
||||||
// DeleteWhere deletes i where key = value
|
// DeleteWhere deletes i where key = value
|
||||||
// If i didn't exist anyway, then no error should be returned.
|
// If i didn't exist anyway, then no error should be returned.
|
||||||
DeleteWhere(key string, value interface{}, i interface{}) error
|
DeleteWhere(where []Where, i interface{}) error
|
||||||
|
|
||||||
/*
|
/*
|
||||||
HANDY SHORTCUTS
|
HANDY SHORTCUTS
|
||||||
|
@ -117,7 +126,9 @@ type DB interface {
|
||||||
|
|
||||||
// AcceptFollowRequest moves a follow request in the database from the follow_requests table to the follows table.
|
// AcceptFollowRequest moves a follow request in the database from the follow_requests table to the follows table.
|
||||||
// In other words, it should create the follow, and delete the existing follow request.
|
// In other words, it should create the follow, and delete the existing follow request.
|
||||||
AcceptFollowRequest(originAccountID string, targetAccountID string) error
|
//
|
||||||
|
// It will return the newly created follow for further processing.
|
||||||
|
AcceptFollowRequest(originAccountID string, targetAccountID string) (*gtsmodel.Follow, error)
|
||||||
|
|
||||||
// CreateInstanceAccount creates an account in the database with the same username as the instance host value.
|
// CreateInstanceAccount creates an account in the database with the same username as the instance host value.
|
||||||
// Ie., if the instance is hosted at 'example.org' the instance user will have a username of 'example.org'.
|
// Ie., if the instance is hosted at 'example.org' the instance user will have a username of 'example.org'.
|
||||||
|
@ -204,6 +215,9 @@ type DB interface {
|
||||||
// That is, it returns true if account1 blocks account2, OR if account2 blocks account1.
|
// That is, it returns true if account1 blocks account2, OR if account2 blocks account1.
|
||||||
Blocked(account1 string, account2 string) (bool, error)
|
Blocked(account1 string, account2 string) (bool, error)
|
||||||
|
|
||||||
|
// GetRelationship retrieves the relationship of the targetAccount to the requestingAccount.
|
||||||
|
GetRelationship(requestingAccount string, targetAccount string) (*gtsmodel.Relationship, error)
|
||||||
|
|
||||||
// StatusVisible returns true if targetStatus is visible to requestingAccount, based on the
|
// StatusVisible returns true if targetStatus is visible to requestingAccount, based on the
|
||||||
// privacy settings of the status, and any blocks/mutes that might exist between the two accounts
|
// privacy settings of the status, and any blocks/mutes that might exist between the two accounts
|
||||||
// or account domains.
|
// or account domains.
|
||||||
|
@ -222,6 +236,9 @@ type DB interface {
|
||||||
// Follows returns true if sourceAccount follows target account, or an error if something goes wrong while finding out.
|
// Follows returns true if sourceAccount follows target account, or an error if something goes wrong while finding out.
|
||||||
Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error)
|
Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error)
|
||||||
|
|
||||||
|
// FollowRequested returns true if sourceAccount has requested to follow target account, or an error if something goes wrong while finding out.
|
||||||
|
FollowRequested(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error)
|
||||||
|
|
||||||
// Mutuals returns true if account1 and account2 both follow each other, or an error if something goes wrong while finding out.
|
// Mutuals returns true if account1 and account2 both follow each other, or an error if something goes wrong while finding out.
|
||||||
Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error)
|
Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error)
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-fed/activity/pub"
|
|
||||||
"github.com/go-pg/pg/extra/pgdebug"
|
"github.com/go-pg/pg/extra/pgdebug"
|
||||||
"github.com/go-pg/pg/v10"
|
"github.com/go-pg/pg/v10"
|
||||||
"github.com/go-pg/pg/v10/orm"
|
"github.com/go-pg/pg/v10/orm"
|
||||||
|
@ -38,7 +37,6 @@ import (
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
@ -46,11 +44,11 @@ import (
|
||||||
|
|
||||||
// postgresService satisfies the DB interface
|
// postgresService satisfies the DB interface
|
||||||
type postgresService struct {
|
type postgresService struct {
|
||||||
config *config.Config
|
config *config.Config
|
||||||
conn *pg.DB
|
conn *pg.DB
|
||||||
log *logrus.Logger
|
log *logrus.Logger
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
federationDB pub.Database
|
// federationDB pub.Database
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPostgresService returns a postgresService derived from the provided config, which implements the go-fed DB interface.
|
// NewPostgresService returns a postgresService derived from the provided config, which implements the go-fed DB interface.
|
||||||
|
@ -97,9 +95,6 @@ func NewPostgresService(ctx context.Context, c *config.Config, log *logrus.Logge
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
}
|
}
|
||||||
|
|
||||||
federatingDB := federation.NewFederatingDB(ps, c, log)
|
|
||||||
ps.federationDB = federatingDB
|
|
||||||
|
|
||||||
// we can confidently return this useable postgres service now
|
// we can confidently return this useable postgres service now
|
||||||
return ps, nil
|
return ps, nil
|
||||||
}
|
}
|
||||||
|
@ -159,14 +154,6 @@ func derivePGOptions(c *config.Config) (*pg.Options, error) {
|
||||||
return options, nil
|
return options, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
FEDERATION FUNCTIONALITY
|
|
||||||
*/
|
|
||||||
|
|
||||||
func (ps *postgresService) Federation() pub.Database {
|
|
||||||
return ps.federationDB
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
BASIC DB FUNCTIONALITY
|
BASIC DB FUNCTIONALITY
|
||||||
*/
|
*/
|
||||||
|
@ -229,8 +216,17 @@ func (ps *postgresService) GetByID(id string, i interface{}) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ps *postgresService) GetWhere(key string, value interface{}, i interface{}) error {
|
func (ps *postgresService) GetWhere(where []db.Where, i interface{}) error {
|
||||||
if err := ps.conn.Model(i).Where("? = ?", pg.Safe(key), value).Select(); err != nil {
|
if len(where) == 0 {
|
||||||
|
return errors.New("no queries provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
q := ps.conn.Model(i)
|
||||||
|
for _, w := range where {
|
||||||
|
q = q.Where("? = ?", pg.Safe(w.Key), w.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := q.Select(); err != nil {
|
||||||
if err == pg.ErrNoRows {
|
if err == pg.ErrNoRows {
|
||||||
return db.ErrNoEntries{}
|
return db.ErrNoEntries{}
|
||||||
}
|
}
|
||||||
|
@ -255,6 +251,9 @@ func (ps *postgresService) GetAll(i interface{}) error {
|
||||||
|
|
||||||
func (ps *postgresService) Put(i interface{}) error {
|
func (ps *postgresService) Put(i interface{}) error {
|
||||||
_, err := ps.conn.Model(i).Insert(i)
|
_, err := ps.conn.Model(i).Insert(i)
|
||||||
|
if err != nil && strings.Contains(err.Error(), "duplicate key value violates unique constraint") {
|
||||||
|
return db.ErrAlreadyExists{}
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,20 +284,31 @@ func (ps *postgresService) UpdateOneByID(id string, key string, value interface{
|
||||||
|
|
||||||
func (ps *postgresService) DeleteByID(id string, i interface{}) error {
|
func (ps *postgresService) DeleteByID(id string, i interface{}) error {
|
||||||
if _, err := ps.conn.Model(i).Where("id = ?", id).Delete(); err != nil {
|
if _, err := ps.conn.Model(i).Where("id = ?", id).Delete(); err != nil {
|
||||||
if err == pg.ErrNoRows {
|
// if there are no rows *anyway* then that's fine
|
||||||
return db.ErrNoEntries{}
|
// just return err if there's an actual error
|
||||||
|
if err != pg.ErrNoRows {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ps *postgresService) DeleteWhere(key string, value interface{}, i interface{}) error {
|
func (ps *postgresService) DeleteWhere(where []db.Where, i interface{}) error {
|
||||||
if _, err := ps.conn.Model(i).Where("? = ?", pg.Safe(key), value).Delete(); err != nil {
|
if len(where) == 0 {
|
||||||
if err == pg.ErrNoRows {
|
return errors.New("no queries provided")
|
||||||
return db.ErrNoEntries{}
|
}
|
||||||
|
|
||||||
|
q := ps.conn.Model(i)
|
||||||
|
for _, w := range where {
|
||||||
|
q = q.Where("? = ?", pg.Safe(w.Key), w.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := q.Delete(); err != nil {
|
||||||
|
// if there are no rows *anyway* then that's fine
|
||||||
|
// just return err if there's an actual error
|
||||||
|
if err != pg.ErrNoRows {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -307,30 +317,34 @@ func (ps *postgresService) DeleteWhere(key string, value interface{}, i interfac
|
||||||
HANDY SHORTCUTS
|
HANDY SHORTCUTS
|
||||||
*/
|
*/
|
||||||
|
|
||||||
func (ps *postgresService) AcceptFollowRequest(originAccountID string, targetAccountID string) error {
|
func (ps *postgresService) AcceptFollowRequest(originAccountID string, targetAccountID string) (*gtsmodel.Follow, error) {
|
||||||
|
// make sure the original follow request exists
|
||||||
fr := >smodel.FollowRequest{}
|
fr := >smodel.FollowRequest{}
|
||||||
if err := ps.conn.Model(fr).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Select(); err != nil {
|
if err := ps.conn.Model(fr).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Select(); err != nil {
|
||||||
if err == pg.ErrMultiRows {
|
if err == pg.ErrMultiRows {
|
||||||
return db.ErrNoEntries{}
|
return nil, db.ErrNoEntries{}
|
||||||
}
|
}
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create a new follow to 'replace' the request with
|
||||||
follow := >smodel.Follow{
|
follow := >smodel.Follow{
|
||||||
AccountID: originAccountID,
|
AccountID: originAccountID,
|
||||||
TargetAccountID: targetAccountID,
|
TargetAccountID: targetAccountID,
|
||||||
URI: fr.URI,
|
URI: fr.URI,
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := ps.conn.Model(follow).Insert(); err != nil {
|
// if the follow already exists, just update the URI -- we don't need to do anything else
|
||||||
return err
|
if _, err := ps.conn.Model(follow).OnConflict("ON CONSTRAINT follows_account_id_target_account_id_key DO UPDATE set uri = ?", follow.URI).Insert(); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// now remove the follow request
|
||||||
if _, err := ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Delete(); err != nil {
|
if _, err := ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Delete(); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return follow, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ps *postgresService) CreateInstanceAccount() error {
|
func (ps *postgresService) CreateInstanceAccount() error {
|
||||||
|
@ -681,6 +695,60 @@ func (ps *postgresService) Blocked(account1 string, account2 string) (bool, erro
|
||||||
return blocked, nil
|
return blocked, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ps *postgresService) GetRelationship(requestingAccount string, targetAccount string) (*gtsmodel.Relationship, error) {
|
||||||
|
r := >smodel.Relationship{
|
||||||
|
ID: targetAccount,
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the requesting account follows the target account
|
||||||
|
follow := >smodel.Follow{}
|
||||||
|
if err := ps.conn.Model(follow).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Select(); err != nil {
|
||||||
|
if err != pg.ErrNoRows {
|
||||||
|
// a proper error
|
||||||
|
return nil, fmt.Errorf("getrelationship: error checking follow existence: %s", err)
|
||||||
|
}
|
||||||
|
// no follow exists so these are all false
|
||||||
|
r.Following = false
|
||||||
|
r.ShowingReblogs = false
|
||||||
|
r.Notifying = false
|
||||||
|
} else {
|
||||||
|
// follow exists so we can fill these fields out...
|
||||||
|
r.Following = true
|
||||||
|
r.ShowingReblogs = follow.ShowReblogs
|
||||||
|
r.Notifying = follow.Notify
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the target account follows the requesting account
|
||||||
|
followedBy, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", targetAccount).Where("target_account_id = ?", requestingAccount).Exists()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("getrelationship: error checking followed_by existence: %s", err)
|
||||||
|
}
|
||||||
|
r.FollowedBy = followedBy
|
||||||
|
|
||||||
|
// check if the requesting account blocks the target account
|
||||||
|
blocking, err := ps.conn.Model(>smodel.Block{}).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Exists()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("getrelationship: error checking blocking existence: %s", err)
|
||||||
|
}
|
||||||
|
r.Blocking = blocking
|
||||||
|
|
||||||
|
// check if the target account blocks the requesting account
|
||||||
|
blockedBy, err := ps.conn.Model(>smodel.Block{}).Where("account_id = ?", targetAccount).Where("target_account_id = ?", requestingAccount).Exists()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("getrelationship: error checking blocked existence: %s", err)
|
||||||
|
}
|
||||||
|
r.BlockedBy = blockedBy
|
||||||
|
|
||||||
|
// check if there's a pending following request from requesting account to target account
|
||||||
|
requested, err := ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Exists()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("getrelationship: error checking blocked existence: %s", err)
|
||||||
|
}
|
||||||
|
r.Requested = requested
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account, relevantAccounts *gtsmodel.RelevantAccounts) (bool, error) {
|
func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account, relevantAccounts *gtsmodel.RelevantAccounts) (bool, error) {
|
||||||
l := ps.log.WithField("func", "StatusVisible")
|
l := ps.log.WithField("func", "StatusVisible")
|
||||||
|
|
||||||
|
@ -853,6 +921,10 @@ func (ps *postgresService) Follows(sourceAccount *gtsmodel.Account, targetAccoun
|
||||||
return ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists()
|
return ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ps *postgresService) FollowRequested(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) {
|
||||||
|
return ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists()
|
||||||
|
}
|
||||||
|
|
||||||
func (ps *postgresService) Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error) {
|
func (ps *postgresService) Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error) {
|
||||||
// make sure account 1 follows account 2
|
// make sure account 1 follows account 2
|
||||||
f1, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", account1.ID).Where("target_account_id = ?", account2.ID).Exists()
|
f1, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", account1.ID).Where("target_account_id = ?", account2.ID).Exists()
|
||||||
|
@ -1036,6 +1108,11 @@ func (ps *postgresService) WhoFavedStatus(status *gtsmodel.Status) ([]*gtsmodel.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, originAccountID string, statusID string) ([]*gtsmodel.Mention, error) {
|
func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, originAccountID string, statusID string) ([]*gtsmodel.Mention, error) {
|
||||||
|
ogAccount := >smodel.Account{}
|
||||||
|
if err := ps.conn.Model(ogAccount).Where("id = ?", originAccountID).Select(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
menchies := []*gtsmodel.Mention{}
|
menchies := []*gtsmodel.Mention{}
|
||||||
for _, a := range targetAccounts {
|
for _, a := range targetAccounts {
|
||||||
// A mentioned account looks like "@test@example.org" or just "@test" for a local account
|
// A mentioned account looks like "@test@example.org" or just "@test" for a local account
|
||||||
|
@ -1093,9 +1170,13 @@ func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, ori
|
||||||
|
|
||||||
// id, createdAt and updatedAt will be populated by the db, so we have everything we need!
|
// id, createdAt and updatedAt will be populated by the db, so we have everything we need!
|
||||||
menchies = append(menchies, >smodel.Mention{
|
menchies = append(menchies, >smodel.Mention{
|
||||||
StatusID: statusID,
|
StatusID: statusID,
|
||||||
OriginAccountID: originAccountID,
|
OriginAccountID: ogAccount.ID,
|
||||||
TargetAccountID: mentionedAccount.ID,
|
OriginAccountURI: ogAccount.URI,
|
||||||
|
TargetAccountID: mentionedAccount.ID,
|
||||||
|
NameString: a,
|
||||||
|
MentionedAccountURI: mentionedAccount.URI,
|
||||||
|
GTSAccount: mentionedAccount,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return menchies, nil
|
return menchies, nil
|
||||||
|
|
|
@ -20,6 +20,7 @@ package federation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -37,6 +38,12 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type FederatingDB interface {
|
||||||
|
pub.Database
|
||||||
|
Undo(ctx context.Context, undo vocab.ActivityStreamsUndo) error
|
||||||
|
Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) error
|
||||||
|
}
|
||||||
|
|
||||||
// FederatingDB uses the underlying DB interface to implement the go-fed pub.Database interface.
|
// FederatingDB uses the underlying DB interface to implement the go-fed pub.Database interface.
|
||||||
// It doesn't care what the underlying implementation of the DB interface is, as long as it works.
|
// It doesn't care what the underlying implementation of the DB interface is, as long as it works.
|
||||||
type federatingDB struct {
|
type federatingDB struct {
|
||||||
|
@ -47,8 +54,8 @@ type federatingDB struct {
|
||||||
typeConverter typeutils.TypeConverter
|
typeConverter typeutils.TypeConverter
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFederatingDB returns a pub.Database interface using the given database, config, and logger.
|
// NewFederatingDB returns a FederatingDB interface using the given database, config, and logger.
|
||||||
func NewFederatingDB(db db.DB, config *config.Config, log *logrus.Logger) pub.Database {
|
func NewFederatingDB(db db.DB, config *config.Config, log *logrus.Logger) FederatingDB {
|
||||||
return &federatingDB{
|
return &federatingDB{
|
||||||
locks: new(sync.Map),
|
locks: new(sync.Map),
|
||||||
db: db,
|
db: db,
|
||||||
|
@ -204,7 +211,7 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err)
|
return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err)
|
||||||
}
|
}
|
||||||
if err := f.db.GetWhere("uri", uid, >smodel.Status{}); err != nil {
|
if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uid}}, >smodel.Status{}); err != nil {
|
||||||
if _, ok := err.(db.ErrNoEntries); ok {
|
if _, ok := err.(db.ErrNoEntries); ok {
|
||||||
// there are no entries for this status
|
// there are no entries for this status
|
||||||
return false, nil
|
return false, nil
|
||||||
|
@ -253,7 +260,7 @@ func (f *federatingDB) ActorForOutbox(c context.Context, outboxIRI *url.URL) (ac
|
||||||
return nil, fmt.Errorf("%s is not an outbox URI", outboxIRI.String())
|
return nil, fmt.Errorf("%s is not an outbox URI", outboxIRI.String())
|
||||||
}
|
}
|
||||||
acct := >smodel.Account{}
|
acct := >smodel.Account{}
|
||||||
if err := f.db.GetWhere("outbox_uri", outboxIRI.String(), acct); err != nil {
|
if err := f.db.GetWhere([]db.Where{{Key: "outbox_uri", Value: outboxIRI.String()}}, acct); err != nil {
|
||||||
if _, ok := err.(db.ErrNoEntries); ok {
|
if _, ok := err.(db.ErrNoEntries); ok {
|
||||||
return nil, fmt.Errorf("no actor found that corresponds to outbox %s", outboxIRI.String())
|
return nil, fmt.Errorf("no actor found that corresponds to outbox %s", outboxIRI.String())
|
||||||
}
|
}
|
||||||
|
@ -278,7 +285,7 @@ func (f *federatingDB) ActorForInbox(c context.Context, inboxIRI *url.URL) (acto
|
||||||
return nil, fmt.Errorf("%s is not an inbox URI", inboxIRI.String())
|
return nil, fmt.Errorf("%s is not an inbox URI", inboxIRI.String())
|
||||||
}
|
}
|
||||||
acct := >smodel.Account{}
|
acct := >smodel.Account{}
|
||||||
if err := f.db.GetWhere("inbox_uri", inboxIRI.String(), acct); err != nil {
|
if err := f.db.GetWhere([]db.Where{{Key: "inbox_uri", Value: inboxIRI.String()}}, acct); err != nil {
|
||||||
if _, ok := err.(db.ErrNoEntries); ok {
|
if _, ok := err.(db.ErrNoEntries); ok {
|
||||||
return nil, fmt.Errorf("no actor found that corresponds to inbox %s", inboxIRI.String())
|
return nil, fmt.Errorf("no actor found that corresponds to inbox %s", inboxIRI.String())
|
||||||
}
|
}
|
||||||
|
@ -304,7 +311,7 @@ func (f *federatingDB) OutboxForInbox(c context.Context, inboxIRI *url.URL) (out
|
||||||
return nil, fmt.Errorf("%s is not an inbox URI", inboxIRI.String())
|
return nil, fmt.Errorf("%s is not an inbox URI", inboxIRI.String())
|
||||||
}
|
}
|
||||||
acct := >smodel.Account{}
|
acct := >smodel.Account{}
|
||||||
if err := f.db.GetWhere("inbox_uri", inboxIRI.String(), acct); err != nil {
|
if err := f.db.GetWhere([]db.Where{{Key: "inbox_uri", Value: inboxIRI.String()}}, acct); err != nil {
|
||||||
if _, ok := err.(db.ErrNoEntries); ok {
|
if _, ok := err.(db.ErrNoEntries); ok {
|
||||||
return nil, fmt.Errorf("no actor found that corresponds to inbox %s", inboxIRI.String())
|
return nil, fmt.Errorf("no actor found that corresponds to inbox %s", inboxIRI.String())
|
||||||
}
|
}
|
||||||
|
@ -343,9 +350,10 @@ func (f *federatingDB) Get(c context.Context, id *url.URL) (value vocab.Type, er
|
||||||
|
|
||||||
if util.IsUserPath(id) {
|
if util.IsUserPath(id) {
|
||||||
acct := >smodel.Account{}
|
acct := >smodel.Account{}
|
||||||
if err := f.db.GetWhere("uri", id.String(), acct); err != nil {
|
if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: id.String()}}, acct); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
l.Debug("is user path! returning account")
|
||||||
return f.typeConverter.AccountToAS(acct)
|
return f.typeConverter.AccountToAS(acct)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -371,27 +379,40 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
|
||||||
"asType": asType.GetTypeName(),
|
"asType": asType.GetTypeName(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
l.Debugf("received CREATE asType %+v", asType)
|
m, err := streams.Serialize(asType)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(m)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Debugf("received CREATE asType %s", string(b))
|
||||||
|
|
||||||
targetAcctI := ctx.Value(util.APAccount)
|
targetAcctI := ctx.Value(util.APAccount)
|
||||||
if targetAcctI == nil {
|
if targetAcctI == nil {
|
||||||
l.Error("target account wasn't set on context")
|
l.Error("target account wasn't set on context")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
targetAcct, ok := targetAcctI.(*gtsmodel.Account)
|
targetAcct, ok := targetAcctI.(*gtsmodel.Account)
|
||||||
if !ok {
|
if !ok {
|
||||||
l.Error("target account was set on context but couldn't be parsed")
|
l.Error("target account was set on context but couldn't be parsed")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fromFederatorChanI := ctx.Value(util.APFromFederatorChanKey)
|
fromFederatorChanI := ctx.Value(util.APFromFederatorChanKey)
|
||||||
if fromFederatorChanI == nil {
|
if fromFederatorChanI == nil {
|
||||||
l.Error("from federator channel wasn't set on context")
|
l.Error("from federator channel wasn't set on context")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
fromFederatorChan, ok := fromFederatorChanI.(chan gtsmodel.FromFederator)
|
fromFederatorChan, ok := fromFederatorChanI.(chan gtsmodel.FromFederator)
|
||||||
if !ok {
|
if !ok {
|
||||||
l.Error("from federator channel was set on context but couldn't be parsed")
|
l.Error("from federator channel was set on context but couldn't be parsed")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
switch gtsmodel.ActivityStreamsActivity(asType.GetTypeName()) {
|
switch asType.GetTypeName() {
|
||||||
case gtsmodel.ActivityStreamsCreate:
|
case gtsmodel.ActivityStreamsCreate:
|
||||||
create, ok := asType.(vocab.ActivityStreamsCreate)
|
create, ok := asType.(vocab.ActivityStreamsCreate)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -399,7 +420,7 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
|
||||||
}
|
}
|
||||||
object := create.GetActivityStreamsObject()
|
object := create.GetActivityStreamsObject()
|
||||||
for objectIter := object.Begin(); objectIter != object.End(); objectIter = objectIter.Next() {
|
for objectIter := object.Begin(); objectIter != object.End(); objectIter = objectIter.Next() {
|
||||||
switch gtsmodel.ActivityStreamsObject(objectIter.GetType().GetTypeName()) {
|
switch objectIter.GetType().GetTypeName() {
|
||||||
case gtsmodel.ActivityStreamsNote:
|
case gtsmodel.ActivityStreamsNote:
|
||||||
note := objectIter.GetActivityStreamsNote()
|
note := objectIter.GetActivityStreamsNote()
|
||||||
status, err := f.typeConverter.ASStatusToStatus(note)
|
status, err := f.typeConverter.ASStatusToStatus(note)
|
||||||
|
@ -407,13 +428,17 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
|
||||||
return fmt.Errorf("error converting note to status: %s", err)
|
return fmt.Errorf("error converting note to status: %s", err)
|
||||||
}
|
}
|
||||||
if err := f.db.Put(status); err != nil {
|
if err := f.db.Put(status); err != nil {
|
||||||
|
if _, ok := err.(db.ErrAlreadyExists); ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return fmt.Errorf("database error inserting status: %s", err)
|
return fmt.Errorf("database error inserting status: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fromFederatorChan <- gtsmodel.FromFederator{
|
fromFederatorChan <- gtsmodel.FromFederator{
|
||||||
APObjectType: gtsmodel.ActivityStreamsNote,
|
APObjectType: gtsmodel.ActivityStreamsNote,
|
||||||
APActivityType: gtsmodel.ActivityStreamsCreate,
|
APActivityType: gtsmodel.ActivityStreamsCreate,
|
||||||
GTSModel: status,
|
GTSModel: status,
|
||||||
|
ReceivingAccount: targetAcct,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -433,7 +458,7 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !targetAcct.Locked {
|
if !targetAcct.Locked {
|
||||||
if err := f.db.AcceptFollowRequest(followRequest.AccountID, followRequest.TargetAccountID); err != nil {
|
if _, err := f.db.AcceptFollowRequest(followRequest.AccountID, followRequest.TargetAccountID); err != nil {
|
||||||
return fmt.Errorf("database error accepting follow request: %s", err)
|
return fmt.Errorf("database error accepting follow request: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -450,14 +475,87 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
|
||||||
// the entire value.
|
// the entire value.
|
||||||
//
|
//
|
||||||
// The library makes this call only after acquiring a lock first.
|
// The library makes this call only after acquiring a lock first.
|
||||||
func (f *federatingDB) Update(c context.Context, asType vocab.Type) error {
|
func (f *federatingDB) Update(ctx context.Context, asType vocab.Type) error {
|
||||||
l := f.log.WithFields(
|
l := f.log.WithFields(
|
||||||
logrus.Fields{
|
logrus.Fields{
|
||||||
"func": "Update",
|
"func": "Update",
|
||||||
"asType": asType.GetTypeName(),
|
"asType": asType.GetTypeName(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
l.Debugf("received UPDATE asType %+v", asType)
|
m, err := streams.Serialize(asType)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(m)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Debugf("received UPDATE asType %s", string(b))
|
||||||
|
|
||||||
|
receivingAcctI := ctx.Value(util.APAccount)
|
||||||
|
if receivingAcctI == nil {
|
||||||
|
l.Error("receiving account wasn't set on context")
|
||||||
|
}
|
||||||
|
receivingAcct, ok := receivingAcctI.(*gtsmodel.Account)
|
||||||
|
if !ok {
|
||||||
|
l.Error("receiving account was set on context but couldn't be parsed")
|
||||||
|
}
|
||||||
|
|
||||||
|
fromFederatorChanI := ctx.Value(util.APFromFederatorChanKey)
|
||||||
|
if fromFederatorChanI == nil {
|
||||||
|
l.Error("from federator channel wasn't set on context")
|
||||||
|
}
|
||||||
|
fromFederatorChan, ok := fromFederatorChanI.(chan gtsmodel.FromFederator)
|
||||||
|
if !ok {
|
||||||
|
l.Error("from federator channel was set on context but couldn't be parsed")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch asType.GetTypeName() {
|
||||||
|
case gtsmodel.ActivityStreamsUpdate:
|
||||||
|
update, ok := asType.(vocab.ActivityStreamsCreate)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("could not convert type to create")
|
||||||
|
}
|
||||||
|
object := update.GetActivityStreamsObject()
|
||||||
|
for objectIter := object.Begin(); objectIter != object.End(); objectIter = objectIter.Next() {
|
||||||
|
switch objectIter.GetType().GetTypeName() {
|
||||||
|
case string(gtsmodel.ActivityStreamsPerson):
|
||||||
|
person := objectIter.GetActivityStreamsPerson()
|
||||||
|
updatedAcct, err := f.typeConverter.ASRepresentationToAccount(person)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error converting person to account: %s", err)
|
||||||
|
}
|
||||||
|
if err := f.db.Put(updatedAcct); err != nil {
|
||||||
|
return fmt.Errorf("database error inserting updated account: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fromFederatorChan <- gtsmodel.FromFederator{
|
||||||
|
APObjectType: gtsmodel.ActivityStreamsProfile,
|
||||||
|
APActivityType: gtsmodel.ActivityStreamsUpdate,
|
||||||
|
GTSModel: updatedAcct,
|
||||||
|
ReceivingAccount: receivingAcct,
|
||||||
|
}
|
||||||
|
|
||||||
|
case string(gtsmodel.ActivityStreamsApplication):
|
||||||
|
application := objectIter.GetActivityStreamsApplication()
|
||||||
|
updatedAcct, err := f.typeConverter.ASRepresentationToAccount(application)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error converting person to account: %s", err)
|
||||||
|
}
|
||||||
|
if err := f.db.Put(updatedAcct); err != nil {
|
||||||
|
return fmt.Errorf("database error inserting updated account: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fromFederatorChan <- gtsmodel.FromFederator{
|
||||||
|
APObjectType: gtsmodel.ActivityStreamsProfile,
|
||||||
|
APActivityType: gtsmodel.ActivityStreamsUpdate,
|
||||||
|
GTSModel: updatedAcct,
|
||||||
|
ReceivingAccount: receivingAcct,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -490,7 +588,7 @@ func (f *federatingDB) GetOutbox(c context.Context, outboxIRI *url.URL) (inbox v
|
||||||
)
|
)
|
||||||
l.Debug("entering GETOUTBOX function")
|
l.Debug("entering GETOUTBOX function")
|
||||||
|
|
||||||
return nil, nil
|
return streams.NewActivityStreamsOrderedCollectionPage(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetOutbox saves the outbox value given from GetOutbox, with new items
|
// SetOutbox saves the outbox value given from GetOutbox, with new items
|
||||||
|
@ -522,9 +620,60 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (id *url.URL, err
|
||||||
"asType": t.GetTypeName(),
|
"asType": t.GetTypeName(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
l.Debugf("received NEWID request for asType %+v", t)
|
m, err := streams.Serialize(t)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(m)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
l.Debugf("received NEWID request for asType %s", string(b))
|
||||||
|
|
||||||
return url.Parse(fmt.Sprintf("%s://%s/", f.config.Protocol, uuid.NewString()))
|
switch t.GetTypeName() {
|
||||||
|
case gtsmodel.ActivityStreamsFollow:
|
||||||
|
// FOLLOW
|
||||||
|
// ID might already be set on a follow we've created, so check it here and return it if it is
|
||||||
|
follow, ok := t.(vocab.ActivityStreamsFollow)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("newid: follow couldn't be parsed into vocab.ActivityStreamsFollow")
|
||||||
|
}
|
||||||
|
idProp := follow.GetJSONLDId()
|
||||||
|
if idProp != nil {
|
||||||
|
if idProp.IsIRI() {
|
||||||
|
return idProp.GetIRI(), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// it's not set so create one based on the actor set on the follow (ie., the followER not the followEE)
|
||||||
|
actorProp := follow.GetActivityStreamsActor()
|
||||||
|
if actorProp != nil {
|
||||||
|
for iter := actorProp.Begin(); iter != actorProp.End(); iter = iter.Next() {
|
||||||
|
// take the IRI of the first actor we can find (there should only be one)
|
||||||
|
if iter.IsIRI() {
|
||||||
|
actorAccount := >smodel.Account{}
|
||||||
|
if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: iter.GetIRI().String()}}, actorAccount); err == nil { // if there's an error here, just use the fallback behavior -- we don't need to return an error here
|
||||||
|
return url.Parse(util.GenerateURIForFollow(actorAccount.Username, f.config.Protocol, f.config.Host))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case gtsmodel.ActivityStreamsNote:
|
||||||
|
// NOTE aka STATUS
|
||||||
|
// ID might already be set on a note we've created, so check it here and return it if it is
|
||||||
|
note, ok := t.(vocab.ActivityStreamsNote)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("newid: follow couldn't be parsed into vocab.ActivityStreamsNote")
|
||||||
|
}
|
||||||
|
idProp := note.GetJSONLDId()
|
||||||
|
if idProp != nil {
|
||||||
|
if idProp.IsIRI() {
|
||||||
|
return idProp.GetIRI(), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback default behavior: just return a random UUID after our protocol and host
|
||||||
|
return url.Parse(fmt.Sprintf("%s://%s/%s", f.config.Protocol, f.config.Host, uuid.NewString()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Followers obtains the Followers Collection for an actor with the
|
// Followers obtains the Followers Collection for an actor with the
|
||||||
|
@ -543,7 +692,7 @@ func (f *federatingDB) Followers(c context.Context, actorIRI *url.URL) (follower
|
||||||
l.Debugf("entering FOLLOWERS function with actorIRI %s", actorIRI.String())
|
l.Debugf("entering FOLLOWERS function with actorIRI %s", actorIRI.String())
|
||||||
|
|
||||||
acct := >smodel.Account{}
|
acct := >smodel.Account{}
|
||||||
if err := f.db.GetWhere("uri", actorIRI.String(), acct); err != nil {
|
if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: actorIRI.String()}}, acct); err != nil {
|
||||||
return nil, fmt.Errorf("db error getting account with uri %s: %s", actorIRI.String(), err)
|
return nil, fmt.Errorf("db error getting account with uri %s: %s", actorIRI.String(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -585,7 +734,7 @@ func (f *federatingDB) Following(c context.Context, actorIRI *url.URL) (followin
|
||||||
l.Debugf("entering FOLLOWING function with actorIRI %s", actorIRI.String())
|
l.Debugf("entering FOLLOWING function with actorIRI %s", actorIRI.String())
|
||||||
|
|
||||||
acct := >smodel.Account{}
|
acct := >smodel.Account{}
|
||||||
if err := f.db.GetWhere("uri", actorIRI.String(), acct); err != nil {
|
if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: actorIRI.String()}}, acct); err != nil {
|
||||||
return nil, fmt.Errorf("db error getting account with uri %s: %s", actorIRI.String(), err)
|
return nil, fmt.Errorf("db error getting account with uri %s: %s", actorIRI.String(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -627,3 +776,139 @@ func (f *federatingDB) Liked(c context.Context, actorIRI *url.URL) (liked vocab.
|
||||||
l.Debugf("entering LIKED function with actorIRI %s", actorIRI.String())
|
l.Debugf("entering LIKED function with actorIRI %s", actorIRI.String())
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
CUSTOM FUNCTIONALITY FOR GTS
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (f *federatingDB) Undo(ctx context.Context, undo vocab.ActivityStreamsUndo) error {
|
||||||
|
l := f.log.WithFields(
|
||||||
|
logrus.Fields{
|
||||||
|
"func": "Undo",
|
||||||
|
"asType": undo.GetTypeName(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
m, err := streams.Serialize(undo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(m)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
l.Debugf("received UNDO asType %s", string(b))
|
||||||
|
|
||||||
|
targetAcctI := ctx.Value(util.APAccount)
|
||||||
|
if targetAcctI == nil {
|
||||||
|
l.Error("UNDO: target account wasn't set on context")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
targetAcct, ok := targetAcctI.(*gtsmodel.Account)
|
||||||
|
if !ok {
|
||||||
|
l.Error("UNDO: target account was set on context but couldn't be parsed")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
undoObject := undo.GetActivityStreamsObject()
|
||||||
|
if undoObject == nil {
|
||||||
|
return errors.New("UNDO: no object set on vocab.ActivityStreamsUndo")
|
||||||
|
}
|
||||||
|
|
||||||
|
for iter := undoObject.Begin(); iter != undoObject.End(); iter = iter.Next() {
|
||||||
|
switch iter.GetType().GetTypeName() {
|
||||||
|
case string(gtsmodel.ActivityStreamsFollow):
|
||||||
|
// UNDO FOLLOW
|
||||||
|
ASFollow, ok := iter.GetType().(vocab.ActivityStreamsFollow)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("UNDO: couldn't parse follow into vocab.ActivityStreamsFollow")
|
||||||
|
}
|
||||||
|
// make sure the actor owns the follow
|
||||||
|
if !sameActor(undo.GetActivityStreamsActor(), ASFollow.GetActivityStreamsActor()) {
|
||||||
|
return errors.New("UNDO: follow actor and activity actor not the same")
|
||||||
|
}
|
||||||
|
// convert the follow to something we can understand
|
||||||
|
gtsFollow, err := f.typeConverter.ASFollowToFollow(ASFollow)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("UNDO: error converting asfollow to gtsfollow: %s", err)
|
||||||
|
}
|
||||||
|
// make sure the addressee of the original follow is the same as whatever inbox this landed in
|
||||||
|
if gtsFollow.TargetAccountID != targetAcct.ID {
|
||||||
|
return errors.New("UNDO: follow object account and inbox account were not the same")
|
||||||
|
}
|
||||||
|
// delete any existing FOLLOW
|
||||||
|
if err := f.db.DeleteWhere([]db.Where{{Key: "uri", Value: gtsFollow.URI}}, >smodel.Follow{}); err != nil {
|
||||||
|
return fmt.Errorf("UNDO: db error removing follow: %s", err)
|
||||||
|
}
|
||||||
|
// delete any existing FOLLOW REQUEST
|
||||||
|
if err := f.db.DeleteWhere([]db.Where{{Key: "uri", Value: gtsFollow.URI}}, >smodel.FollowRequest{}); err != nil {
|
||||||
|
return fmt.Errorf("UNDO: db error removing follow request: %s", err)
|
||||||
|
}
|
||||||
|
l.Debug("follow undone")
|
||||||
|
return nil
|
||||||
|
case string(gtsmodel.ActivityStreamsLike):
|
||||||
|
// UNDO LIKE
|
||||||
|
case string(gtsmodel.ActivityStreamsAnnounce):
|
||||||
|
// UNDO BOOST/REBLOG/ANNOUNCE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) error {
|
||||||
|
l := f.log.WithFields(
|
||||||
|
logrus.Fields{
|
||||||
|
"func": "Accept",
|
||||||
|
"asType": accept.GetTypeName(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
m, err := streams.Serialize(accept)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(m)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
l.Debugf("received ACCEPT asType %s", string(b))
|
||||||
|
|
||||||
|
inboxAcctI := ctx.Value(util.APAccount)
|
||||||
|
if inboxAcctI == nil {
|
||||||
|
l.Error("ACCEPT: inbox account wasn't set on context")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
inboxAcct, ok := inboxAcctI.(*gtsmodel.Account)
|
||||||
|
if !ok {
|
||||||
|
l.Error("ACCEPT: inbox account was set on context but couldn't be parsed")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
acceptObject := accept.GetActivityStreamsObject()
|
||||||
|
if acceptObject == nil {
|
||||||
|
return errors.New("ACCEPT: no object set on vocab.ActivityStreamsUndo")
|
||||||
|
}
|
||||||
|
|
||||||
|
for iter := acceptObject.Begin(); iter != acceptObject.End(); iter = iter.Next() {
|
||||||
|
switch iter.GetType().GetTypeName() {
|
||||||
|
case string(gtsmodel.ActivityStreamsFollow):
|
||||||
|
// ACCEPT FOLLOW
|
||||||
|
asFollow, ok := iter.GetType().(vocab.ActivityStreamsFollow)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("ACCEPT: couldn't parse follow into vocab.ActivityStreamsFollow")
|
||||||
|
}
|
||||||
|
// convert the follow to something we can understand
|
||||||
|
gtsFollow, err := f.typeConverter.ASFollowToFollow(asFollow)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ACCEPT: error converting asfollow to gtsfollow: %s", err)
|
||||||
|
}
|
||||||
|
// make sure the addressee of the original follow is the same as whatever inbox this landed in
|
||||||
|
if gtsFollow.AccountID != inboxAcct.ID {
|
||||||
|
return errors.New("ACCEPT: follow object account and inbox account were not the same")
|
||||||
|
}
|
||||||
|
_, err = f.db.AcceptFollowRequest(gtsFollow.AccountID, gtsFollow.TargetAccountID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -124,7 +124,7 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr
|
||||||
}
|
}
|
||||||
|
|
||||||
requestingAccount := >smodel.Account{}
|
requestingAccount := >smodel.Account{}
|
||||||
if err := f.db.GetWhere("uri", publicKeyOwnerURI.String(), requestingAccount); err != nil {
|
if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: publicKeyOwnerURI.String()}}, requestingAccount); err != nil {
|
||||||
// there's been a proper error so return it
|
// there's been a proper error so return it
|
||||||
if _, ok := err.(db.ErrNoEntries); !ok {
|
if _, ok := err.(db.ErrNoEntries); !ok {
|
||||||
return ctx, false, fmt.Errorf("error getting requesting account with public key id %s: %s", publicKeyOwnerURI.String(), err)
|
return ctx, false, fmt.Errorf("error getting requesting account with public key id %s: %s", publicKeyOwnerURI.String(), err)
|
||||||
|
@ -146,6 +146,22 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr
|
||||||
}
|
}
|
||||||
|
|
||||||
requestingAccount = a
|
requestingAccount = a
|
||||||
|
|
||||||
|
// send the newly dereferenced account into the processor channel for further async processing
|
||||||
|
fromFederatorChanI := ctx.Value(util.APFromFederatorChanKey)
|
||||||
|
if fromFederatorChanI == nil {
|
||||||
|
l.Error("from federator channel wasn't set on context")
|
||||||
|
}
|
||||||
|
fromFederatorChan, ok := fromFederatorChanI.(chan gtsmodel.FromFederator)
|
||||||
|
if !ok {
|
||||||
|
l.Error("from federator channel was set on context but couldn't be parsed")
|
||||||
|
}
|
||||||
|
|
||||||
|
fromFederatorChan <- gtsmodel.FromFederator{
|
||||||
|
APObjectType: gtsmodel.ActivityStreamsProfile,
|
||||||
|
APActivityType: gtsmodel.ActivityStreamsCreate,
|
||||||
|
GTSModel: requestingAccount,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
withRequester := context.WithValue(ctx, util.APRequestingAccount, requestingAccount)
|
withRequester := context.WithValue(ctx, util.APRequestingAccount, requestingAccount)
|
||||||
|
@ -184,7 +200,7 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er
|
||||||
|
|
||||||
for _, uri := range actorIRIs {
|
for _, uri := range actorIRIs {
|
||||||
a := >smodel.Account{}
|
a := >smodel.Account{}
|
||||||
if err := f.db.GetWhere("uri", uri.String(), a); err != nil {
|
if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, a); 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
|
||||||
|
@ -228,17 +244,19 @@ func (f *federator) FederatingCallbacks(ctx context.Context) (wrapped pub.Federa
|
||||||
"func": "FederatingCallbacks",
|
"func": "FederatingCallbacks",
|
||||||
})
|
})
|
||||||
|
|
||||||
targetAcctI := ctx.Value(util.APAccount)
|
receivingAcctI := ctx.Value(util.APAccount)
|
||||||
if targetAcctI == nil {
|
if receivingAcctI == nil {
|
||||||
l.Error("target account wasn't set on context")
|
l.Error("receiving account wasn't set on context")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
targetAcct, ok := targetAcctI.(*gtsmodel.Account)
|
receivingAcct, ok := receivingAcctI.(*gtsmodel.Account)
|
||||||
if !ok {
|
if !ok {
|
||||||
l.Error("target account was set on context but couldn't be parsed")
|
l.Error("receiving account was set on context but couldn't be parsed")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var onFollow pub.OnFollowBehavior = pub.OnFollowAutomaticallyAccept
|
var onFollow pub.OnFollowBehavior = pub.OnFollowAutomaticallyAccept
|
||||||
if targetAcct.Locked {
|
if receivingAcct.Locked {
|
||||||
onFollow = pub.OnFollowDoNothing
|
onFollow = pub.OnFollowDoNothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,6 +266,17 @@ func (f *federator) FederatingCallbacks(ctx context.Context) (wrapped pub.Federa
|
||||||
OnFollow: onFollow,
|
OnFollow: onFollow,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
other = []interface{}{
|
||||||
|
// override default undo behavior
|
||||||
|
func(ctx context.Context, undo vocab.ActivityStreamsUndo) error {
|
||||||
|
return f.FederatingDB().Undo(ctx, undo)
|
||||||
|
},
|
||||||
|
// override default accept behavior
|
||||||
|
func(ctx context.Context, accept vocab.ActivityStreamsAccept) error {
|
||||||
|
return f.FederatingDB().Accept(ctx, accept)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,8 @@ import (
|
||||||
type Federator interface {
|
type Federator interface {
|
||||||
// FederatingActor returns the underlying pub.FederatingActor, which can be used to send activities, and serve actors at inboxes/outboxes.
|
// FederatingActor returns the underlying pub.FederatingActor, which can be used to send activities, and serve actors at inboxes/outboxes.
|
||||||
FederatingActor() pub.FederatingActor
|
FederatingActor() pub.FederatingActor
|
||||||
|
// FederatingDB returns the underlying FederatingDB interface.
|
||||||
|
FederatingDB() FederatingDB
|
||||||
// AuthenticateFederatedRequest can be used to check the authenticity of incoming http-signed requests for federating resources.
|
// AuthenticateFederatedRequest can be used to check the authenticity of incoming http-signed requests for federating resources.
|
||||||
// The given username will be used to create a transport for making outgoing requests. See the implementation for more detailed comments.
|
// The given username will be used to create a transport for making outgoing requests. See the implementation for more detailed comments.
|
||||||
AuthenticateFederatedRequest(username string, r *http.Request) (*url.URL, error)
|
AuthenticateFederatedRequest(username string, r *http.Request) (*url.URL, error)
|
||||||
|
@ -52,6 +54,7 @@ type Federator interface {
|
||||||
type federator struct {
|
type federator struct {
|
||||||
config *config.Config
|
config *config.Config
|
||||||
db db.DB
|
db db.DB
|
||||||
|
federatingDB FederatingDB
|
||||||
clock pub.Clock
|
clock pub.Clock
|
||||||
typeConverter typeutils.TypeConverter
|
typeConverter typeutils.TypeConverter
|
||||||
transportController transport.Controller
|
transportController transport.Controller
|
||||||
|
@ -60,18 +63,19 @@ type federator struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFederator returns a new federator
|
// NewFederator returns a new federator
|
||||||
func NewFederator(db db.DB, transportController transport.Controller, config *config.Config, log *logrus.Logger, typeConverter typeutils.TypeConverter) Federator {
|
func NewFederator(db db.DB, federatingDB FederatingDB, transportController transport.Controller, config *config.Config, log *logrus.Logger, typeConverter typeutils.TypeConverter) Federator {
|
||||||
|
|
||||||
clock := &Clock{}
|
clock := &Clock{}
|
||||||
f := &federator{
|
f := &federator{
|
||||||
config: config,
|
config: config,
|
||||||
db: db,
|
db: db,
|
||||||
|
federatingDB: federatingDB,
|
||||||
clock: &Clock{},
|
clock: &Clock{},
|
||||||
typeConverter: typeConverter,
|
typeConverter: typeConverter,
|
||||||
transportController: transportController,
|
transportController: transportController,
|
||||||
log: log,
|
log: log,
|
||||||
}
|
}
|
||||||
actor := newFederatingActor(f, f, db.Federation(), clock)
|
actor := newFederatingActor(f, f, federatingDB, clock)
|
||||||
f.actor = actor
|
f.actor = actor
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
@ -79,3 +83,7 @@ func NewFederator(db db.DB, transportController transport.Controller, config *co
|
||||||
func (f *federator) FederatingActor() pub.FederatingActor {
|
func (f *federator) FederatingActor() pub.FederatingActor {
|
||||||
return f.actor
|
return f.actor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *federator) FederatingDB() FederatingDB {
|
||||||
|
return f.federatingDB
|
||||||
|
}
|
||||||
|
|
|
@ -89,7 +89,7 @@ func (suite *ProtocolTestSuite) TestPostInboxRequestBodyHook() {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}))
|
}))
|
||||||
// setup module being tested
|
// setup module being tested
|
||||||
federator := federation.NewFederator(suite.db, tc, suite.config, suite.log, suite.typeConverter)
|
federator := federation.NewFederator(suite.db, testrig.NewTestFederatingDB(suite.db), tc, suite.config, suite.log, suite.typeConverter)
|
||||||
|
|
||||||
// setup request
|
// setup request
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
@ -155,7 +155,7 @@ func (suite *ProtocolTestSuite) TestAuthenticatePostInbox() {
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// now setup module being tested, with the mock transport controller
|
// now setup module being tested, with the mock transport controller
|
||||||
federator := federation.NewFederator(suite.db, tc, suite.config, suite.log, suite.typeConverter)
|
federator := federation.NewFederator(suite.db, testrig.NewTestFederatingDB(suite.db), tc, suite.config, suite.log, suite.typeConverter)
|
||||||
|
|
||||||
// setup request
|
// setup request
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
|
@ -27,11 +27,13 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/go-fed/activity/pub"
|
"github.com/go-fed/activity/pub"
|
||||||
"github.com/go-fed/activity/streams"
|
"github.com/go-fed/activity/streams"
|
||||||
"github.com/go-fed/activity/streams/vocab"
|
"github.com/go-fed/activity/streams/vocab"
|
||||||
"github.com/go-fed/httpsig"
|
"github.com/go-fed/httpsig"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||||
|
@ -128,57 +130,73 @@ func (f *federator) AuthenticateFederatedRequest(username string, r *http.Reques
|
||||||
return nil, fmt.Errorf("could not parse key id into a url: %s", err)
|
return nil, fmt.Errorf("could not parse key id into a url: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
transport, err := f.GetTransportForUser(username)
|
var publicKey interface{}
|
||||||
if err != nil {
|
var pkOwnerURI *url.URL
|
||||||
return nil, fmt.Errorf("transport err: %s", err)
|
if strings.EqualFold(requestingPublicKeyID.Host, f.config.Host) {
|
||||||
}
|
// the request is coming from INSIDE THE HOUSE so skip the remote dereferencing
|
||||||
|
requestingLocalAccount := >smodel.Account{}
|
||||||
|
if err := f.db.GetWhere([]db.Where{{Key: "public_key_uri", Value: requestingPublicKeyID.String()}}, requestingLocalAccount); err != nil {
|
||||||
|
return nil, fmt.Errorf("couldn't get local account with public key uri %s from the database: %s", requestingPublicKeyID.String(), err)
|
||||||
|
}
|
||||||
|
publicKey = requestingLocalAccount.PublicKey
|
||||||
|
pkOwnerURI, err = url.Parse(requestingLocalAccount.URI)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing url %s: %s", requestingLocalAccount.URI, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// the request is remote, so we need to authenticate the request properly by dereferencing the remote key
|
||||||
|
transport, err := f.GetTransportForUser(username)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("transport err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
// The actual http call to the remote server is made right here in the Dereference function.
|
// The actual http call to the remote server is made right here in the Dereference function.
|
||||||
b, err := transport.Dereference(context.Background(), requestingPublicKeyID)
|
b, err := transport.Dereference(context.Background(), requestingPublicKeyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error deferencing key %s: %s", requestingPublicKeyID.String(), err)
|
return nil, fmt.Errorf("error deferencing key %s: %s", requestingPublicKeyID.String(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the key isn't in the response, we can't authenticate the request
|
// if the key isn't in the response, we can't authenticate the request
|
||||||
requestingPublicKey, err := getPublicKeyFromResponse(context.Background(), b, requestingPublicKeyID)
|
requestingPublicKey, err := getPublicKeyFromResponse(context.Background(), b, requestingPublicKeyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error getting key %s from response %s: %s", requestingPublicKeyID.String(), string(b), err)
|
return nil, fmt.Errorf("error getting key %s from response %s: %s", requestingPublicKeyID.String(), string(b), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// we should be able to get the actual key embedded in the vocab.W3IDSecurityV1PublicKey
|
// we should be able to get the actual key embedded in the vocab.W3IDSecurityV1PublicKey
|
||||||
pkPemProp := requestingPublicKey.GetW3IDSecurityV1PublicKeyPem()
|
pkPemProp := requestingPublicKey.GetW3IDSecurityV1PublicKeyPem()
|
||||||
if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() {
|
if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() {
|
||||||
return nil, errors.New("publicKeyPem property is not provided or it is not embedded as a value")
|
return nil, errors.New("publicKeyPem property is not provided or it is not embedded as a value")
|
||||||
}
|
}
|
||||||
|
|
||||||
// and decode the PEM so that we can parse it as a golang public key
|
// and decode the PEM so that we can parse it as a golang public key
|
||||||
pubKeyPem := pkPemProp.Get()
|
pubKeyPem := pkPemProp.Get()
|
||||||
block, _ := pem.Decode([]byte(pubKeyPem))
|
block, _ := pem.Decode([]byte(pubKeyPem))
|
||||||
if block == nil || block.Type != "PUBLIC KEY" {
|
if block == nil || block.Type != "PUBLIC KEY" {
|
||||||
return nil, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type")
|
return nil, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type")
|
||||||
}
|
}
|
||||||
|
|
||||||
p, err := x509.ParsePKIXPublicKey(block.Bytes)
|
publicKey, err = x509.ParsePKIXPublicKey(block.Bytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not parse public key from block bytes: %s", err)
|
return nil, fmt.Errorf("could not parse public key from block bytes: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// all good! we just need the URI of the key owner to return
|
||||||
|
pkOwnerProp := requestingPublicKey.GetW3IDSecurityV1Owner()
|
||||||
|
if pkOwnerProp == nil || !pkOwnerProp.IsIRI() {
|
||||||
|
return nil, errors.New("publicKeyOwner property is not provided or it is not embedded as a value")
|
||||||
|
}
|
||||||
|
pkOwnerURI = pkOwnerProp.GetIRI()
|
||||||
}
|
}
|
||||||
if p == nil {
|
if publicKey == nil {
|
||||||
return nil, errors.New("returned public key was empty")
|
return nil, errors.New("returned public key was empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
// do the actual authentication here!
|
// do the actual authentication here!
|
||||||
algo := httpsig.RSA_SHA256 // TODO: make this more robust
|
algo := httpsig.RSA_SHA256 // TODO: make this more robust
|
||||||
if err := verifier.Verify(p, algo); err != nil {
|
if err := verifier.Verify(publicKey, algo); err != nil {
|
||||||
return nil, fmt.Errorf("error verifying key %s: %s", requestingPublicKeyID.String(), err)
|
return nil, fmt.Errorf("error verifying key %s: %s", requestingPublicKeyID.String(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// all good! we just need the URI of the key owner to return
|
|
||||||
pkOwnerProp := requestingPublicKey.GetW3IDSecurityV1Owner()
|
|
||||||
if pkOwnerProp == nil || !pkOwnerProp.IsIRI() {
|
|
||||||
return nil, errors.New("publicKeyOwner property is not provided or it is not embedded as a value")
|
|
||||||
}
|
|
||||||
pkOwnerURI := pkOwnerProp.GetIRI()
|
|
||||||
|
|
||||||
return pkOwnerURI, nil
|
return pkOwnerURI, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,6 +235,12 @@ func (f *federator) DereferenceRemoteAccount(username string, remoteAccountID *u
|
||||||
return nil, errors.New("error resolving type as activitystreams application")
|
return nil, errors.New("error resolving type as activitystreams application")
|
||||||
}
|
}
|
||||||
return p, nil
|
return p, nil
|
||||||
|
case string(gtsmodel.ActivityStreamsService):
|
||||||
|
p, ok := t.(vocab.ActivityStreamsService)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("error resolving type as activitystreams service")
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("type name %s not supported", t.GetTypeName())
|
return nil, fmt.Errorf("type name %s not supported", t.GetTypeName())
|
||||||
|
@ -243,3 +267,23 @@ func (f *federator) GetTransportForUser(username string) (transport.Transport, e
|
||||||
}
|
}
|
||||||
return transport, nil
|
return transport, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sameActor(activityActor vocab.ActivityStreamsActorProperty, followActor vocab.ActivityStreamsActorProperty) bool {
|
||||||
|
if activityActor == nil || followActor == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for aIter := activityActor.Begin(); aIter != activityActor.End(); aIter = aIter.Next() {
|
||||||
|
for fIter := followActor.Begin(); fIter != followActor.End(); fIter = fIter.Next() {
|
||||||
|
if aIter.GetIRI() == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if fIter.GetIRI() == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if aIter.GetIRI().String() == fIter.GetIRI().String() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
@ -83,6 +83,8 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr
|
||||||
return fmt.Errorf("error creating dbservice: %s", err)
|
return fmt.Errorf("error creating dbservice: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
federatingDB := federation.NewFederatingDB(dbService, c, log)
|
||||||
|
|
||||||
router, err := router.New(c, log)
|
router, err := router.New(c, log)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error creating router: %s", err)
|
return fmt.Errorf("error creating router: %s", err)
|
||||||
|
@ -100,7 +102,7 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr
|
||||||
mediaHandler := media.New(c, dbService, storageBackend, log)
|
mediaHandler := media.New(c, dbService, storageBackend, log)
|
||||||
oauthServer := oauth.New(dbService, log)
|
oauthServer := oauth.New(dbService, log)
|
||||||
transportController := transport.NewController(c, &federation.Clock{}, http.DefaultClient, log)
|
transportController := transport.NewController(c, &federation.Clock{}, http.DefaultClient, log)
|
||||||
federator := federation.NewFederator(dbService, transportController, c, log, typeConverter)
|
federator := federation.NewFederator(dbService, federatingDB, transportController, c, log, typeConverter)
|
||||||
processor := message.NewProcessor(c, typeConverter, federator, oauthServer, mediaHandler, storageBackend, dbService, log)
|
processor := message.NewProcessor(c, typeConverter, federator, oauthServer, mediaHandler, storageBackend, dbService, log)
|
||||||
if err := processor.Start(); err != nil {
|
if err := processor.Start(); err != nil {
|
||||||
return fmt.Errorf("error starting processor: %s", err)
|
return fmt.Errorf("error starting processor: %s", err)
|
||||||
|
|
|
@ -76,13 +76,13 @@ type Account struct {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Does this account need an approval for new followers?
|
// Does this account need an approval for new followers?
|
||||||
Locked bool `pg:",default:'true'"`
|
Locked bool
|
||||||
// Should this account be shown in the instance's profile directory?
|
// Should this account be shown in the instance's profile directory?
|
||||||
Discoverable bool
|
Discoverable bool
|
||||||
// Default post privacy for this account
|
// Default post privacy for this account
|
||||||
Privacy Visibility
|
Privacy Visibility
|
||||||
// Set posts from this account to sensitive by default?
|
// Set posts from this account to sensitive by default?
|
||||||
Sensitive bool `pg:",default:'false'"`
|
Sensitive bool
|
||||||
// What language does this account post in?
|
// What language does this account post in?
|
||||||
Language string `pg:",default:'en'"`
|
Language string `pg:",default:'en'"`
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ type Account struct {
|
||||||
// URL for getting the featured collection list of this account
|
// URL for getting the featured collection list of this account
|
||||||
FeaturedCollectionURI string `pg:",unique"`
|
FeaturedCollectionURI string `pg:",unique"`
|
||||||
// What type of activitypub actor is this account?
|
// What type of activitypub actor is this account?
|
||||||
ActorType ActivityStreamsActor
|
ActorType string
|
||||||
// This account is associated with x account id
|
// This account is associated with x account id
|
||||||
AlsoKnownAs string
|
AlsoKnownAs string
|
||||||
|
|
||||||
|
|
|
@ -18,110 +18,101 @@
|
||||||
|
|
||||||
package gtsmodel
|
package gtsmodel
|
||||||
|
|
||||||
// ActivityStreamsObject refers to https://www.w3.org/TR/activitystreams-vocabulary/#object-types
|
|
||||||
type ActivityStreamsObject string
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// ActivityStreamsArticle https://www.w3.org/TR/activitystreams-vocabulary/#dfn-article
|
// ActivityStreamsArticle https://www.w3.org/TR/activitystreams-vocabulary/#dfn-article
|
||||||
ActivityStreamsArticle ActivityStreamsObject = "Article"
|
ActivityStreamsArticle = "Article"
|
||||||
// ActivityStreamsAudio https://www.w3.org/TR/activitystreams-vocabulary/#dfn-audio
|
// ActivityStreamsAudio https://www.w3.org/TR/activitystreams-vocabulary/#dfn-audio
|
||||||
ActivityStreamsAudio ActivityStreamsObject = "Audio"
|
ActivityStreamsAudio = "Audio"
|
||||||
// ActivityStreamsDocument https://www.w3.org/TR/activitystreams-vocabulary/#dfn-document
|
// ActivityStreamsDocument https://www.w3.org/TR/activitystreams-vocabulary/#dfn-document
|
||||||
ActivityStreamsDocument ActivityStreamsObject = "Event"
|
ActivityStreamsDocument = "Event"
|
||||||
// ActivityStreamsEvent https://www.w3.org/TR/activitystreams-vocabulary/#dfn-event
|
// ActivityStreamsEvent https://www.w3.org/TR/activitystreams-vocabulary/#dfn-event
|
||||||
ActivityStreamsEvent ActivityStreamsObject = "Event"
|
ActivityStreamsEvent = "Event"
|
||||||
// ActivityStreamsImage https://www.w3.org/TR/activitystreams-vocabulary/#dfn-image
|
// ActivityStreamsImage https://www.w3.org/TR/activitystreams-vocabulary/#dfn-image
|
||||||
ActivityStreamsImage ActivityStreamsObject = "Image"
|
ActivityStreamsImage = "Image"
|
||||||
// ActivityStreamsNote https://www.w3.org/TR/activitystreams-vocabulary/#dfn-note
|
// ActivityStreamsNote https://www.w3.org/TR/activitystreams-vocabulary/#dfn-note
|
||||||
ActivityStreamsNote ActivityStreamsObject = "Note"
|
ActivityStreamsNote = "Note"
|
||||||
// ActivityStreamsPage https://www.w3.org/TR/activitystreams-vocabulary/#dfn-page
|
// ActivityStreamsPage https://www.w3.org/TR/activitystreams-vocabulary/#dfn-page
|
||||||
ActivityStreamsPage ActivityStreamsObject = "Page"
|
ActivityStreamsPage = "Page"
|
||||||
// ActivityStreamsPlace https://www.w3.org/TR/activitystreams-vocabulary/#dfn-place
|
// ActivityStreamsPlace https://www.w3.org/TR/activitystreams-vocabulary/#dfn-place
|
||||||
ActivityStreamsPlace ActivityStreamsObject = "Place"
|
ActivityStreamsPlace = "Place"
|
||||||
// ActivityStreamsProfile https://www.w3.org/TR/activitystreams-vocabulary/#dfn-profile
|
// ActivityStreamsProfile https://www.w3.org/TR/activitystreams-vocabulary/#dfn-profile
|
||||||
ActivityStreamsProfile ActivityStreamsObject = "Profile"
|
ActivityStreamsProfile = "Profile"
|
||||||
// ActivityStreamsRelationship https://www.w3.org/TR/activitystreams-vocabulary/#dfn-relationship
|
// ActivityStreamsRelationship https://www.w3.org/TR/activitystreams-vocabulary/#dfn-relationship
|
||||||
ActivityStreamsRelationship ActivityStreamsObject = "Relationship"
|
ActivityStreamsRelationship = "Relationship"
|
||||||
// ActivityStreamsTombstone https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tombstone
|
// ActivityStreamsTombstone https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tombstone
|
||||||
ActivityStreamsTombstone ActivityStreamsObject = "Tombstone"
|
ActivityStreamsTombstone = "Tombstone"
|
||||||
// ActivityStreamsVideo https://www.w3.org/TR/activitystreams-vocabulary/#dfn-video
|
// ActivityStreamsVideo https://www.w3.org/TR/activitystreams-vocabulary/#dfn-video
|
||||||
ActivityStreamsVideo ActivityStreamsObject = "Video"
|
ActivityStreamsVideo = "Video"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ActivityStreamsActor refers to https://www.w3.org/TR/activitystreams-vocabulary/#actor-types
|
|
||||||
type ActivityStreamsActor string
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// ActivityStreamsApplication https://www.w3.org/TR/activitystreams-vocabulary/#dfn-application
|
// ActivityStreamsApplication https://www.w3.org/TR/activitystreams-vocabulary/#dfn-application
|
||||||
ActivityStreamsApplication ActivityStreamsActor = "Application"
|
ActivityStreamsApplication = "Application"
|
||||||
// ActivityStreamsGroup https://www.w3.org/TR/activitystreams-vocabulary/#dfn-group
|
// ActivityStreamsGroup https://www.w3.org/TR/activitystreams-vocabulary/#dfn-group
|
||||||
ActivityStreamsGroup ActivityStreamsActor = "Group"
|
ActivityStreamsGroup = "Group"
|
||||||
// ActivityStreamsOrganization https://www.w3.org/TR/activitystreams-vocabulary/#dfn-organization
|
// ActivityStreamsOrganization https://www.w3.org/TR/activitystreams-vocabulary/#dfn-organization
|
||||||
ActivityStreamsOrganization ActivityStreamsActor = "Organization"
|
ActivityStreamsOrganization = "Organization"
|
||||||
// ActivityStreamsPerson https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person
|
// ActivityStreamsPerson https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person
|
||||||
ActivityStreamsPerson ActivityStreamsActor = "Person"
|
ActivityStreamsPerson = "Person"
|
||||||
// ActivityStreamsService https://www.w3.org/TR/activitystreams-vocabulary/#dfn-service
|
// ActivityStreamsService https://www.w3.org/TR/activitystreams-vocabulary/#dfn-service
|
||||||
ActivityStreamsService ActivityStreamsActor = "Service"
|
ActivityStreamsService = "Service"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ActivityStreamsActivity refers to https://www.w3.org/TR/activitystreams-vocabulary/#activity-types
|
|
||||||
type ActivityStreamsActivity string
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// ActivityStreamsAccept https://www.w3.org/TR/activitystreams-vocabulary/#dfn-accept
|
// ActivityStreamsAccept https://www.w3.org/TR/activitystreams-vocabulary/#dfn-accept
|
||||||
ActivityStreamsAccept ActivityStreamsActivity = "Accept"
|
ActivityStreamsAccept = "Accept"
|
||||||
// ActivityStreamsAdd https://www.w3.org/TR/activitystreams-vocabulary/#dfn-add
|
// ActivityStreamsAdd https://www.w3.org/TR/activitystreams-vocabulary/#dfn-add
|
||||||
ActivityStreamsAdd ActivityStreamsActivity = "Add"
|
ActivityStreamsAdd = "Add"
|
||||||
// ActivityStreamsAnnounce https://www.w3.org/TR/activitystreams-vocabulary/#dfn-announce
|
// ActivityStreamsAnnounce https://www.w3.org/TR/activitystreams-vocabulary/#dfn-announce
|
||||||
ActivityStreamsAnnounce ActivityStreamsActivity = "Announce"
|
ActivityStreamsAnnounce = "Announce"
|
||||||
// ActivityStreamsArrive https://www.w3.org/TR/activitystreams-vocabulary/#dfn-arrive
|
// ActivityStreamsArrive https://www.w3.org/TR/activitystreams-vocabulary/#dfn-arrive
|
||||||
ActivityStreamsArrive ActivityStreamsActivity = "Arrive"
|
ActivityStreamsArrive = "Arrive"
|
||||||
// ActivityStreamsBlock https://www.w3.org/TR/activitystreams-vocabulary/#dfn-block
|
// ActivityStreamsBlock https://www.w3.org/TR/activitystreams-vocabulary/#dfn-block
|
||||||
ActivityStreamsBlock ActivityStreamsActivity = "Block"
|
ActivityStreamsBlock = "Block"
|
||||||
// ActivityStreamsCreate https://www.w3.org/TR/activitystreams-vocabulary/#dfn-create
|
// ActivityStreamsCreate https://www.w3.org/TR/activitystreams-vocabulary/#dfn-create
|
||||||
ActivityStreamsCreate ActivityStreamsActivity = "Create"
|
ActivityStreamsCreate = "Create"
|
||||||
// ActivityStreamsDelete https://www.w3.org/TR/activitystreams-vocabulary/#dfn-delete
|
// ActivityStreamsDelete https://www.w3.org/TR/activitystreams-vocabulary/#dfn-delete
|
||||||
ActivityStreamsDelete ActivityStreamsActivity = "Delete"
|
ActivityStreamsDelete = "Delete"
|
||||||
// ActivityStreamsDislike https://www.w3.org/TR/activitystreams-vocabulary/#dfn-dislike
|
// ActivityStreamsDislike https://www.w3.org/TR/activitystreams-vocabulary/#dfn-dislike
|
||||||
ActivityStreamsDislike ActivityStreamsActivity = "Dislike"
|
ActivityStreamsDislike = "Dislike"
|
||||||
// ActivityStreamsFlag https://www.w3.org/TR/activitystreams-vocabulary/#dfn-flag
|
// ActivityStreamsFlag https://www.w3.org/TR/activitystreams-vocabulary/#dfn-flag
|
||||||
ActivityStreamsFlag ActivityStreamsActivity = "Flag"
|
ActivityStreamsFlag = "Flag"
|
||||||
// ActivityStreamsFollow https://www.w3.org/TR/activitystreams-vocabulary/#dfn-follow
|
// ActivityStreamsFollow https://www.w3.org/TR/activitystreams-vocabulary/#dfn-follow
|
||||||
ActivityStreamsFollow ActivityStreamsActivity = "Follow"
|
ActivityStreamsFollow = "Follow"
|
||||||
// ActivityStreamsIgnore https://www.w3.org/TR/activitystreams-vocabulary/#dfn-ignore
|
// ActivityStreamsIgnore https://www.w3.org/TR/activitystreams-vocabulary/#dfn-ignore
|
||||||
ActivityStreamsIgnore ActivityStreamsActivity = "Ignore"
|
ActivityStreamsIgnore = "Ignore"
|
||||||
// ActivityStreamsInvite https://www.w3.org/TR/activitystreams-vocabulary/#dfn-invite
|
// ActivityStreamsInvite https://www.w3.org/TR/activitystreams-vocabulary/#dfn-invite
|
||||||
ActivityStreamsInvite ActivityStreamsActivity = "Invite"
|
ActivityStreamsInvite = "Invite"
|
||||||
// ActivityStreamsJoin https://www.w3.org/TR/activitystreams-vocabulary/#dfn-join
|
// ActivityStreamsJoin https://www.w3.org/TR/activitystreams-vocabulary/#dfn-join
|
||||||
ActivityStreamsJoin ActivityStreamsActivity = "Join"
|
ActivityStreamsJoin = "Join"
|
||||||
// ActivityStreamsLeave https://www.w3.org/TR/activitystreams-vocabulary/#dfn-leave
|
// ActivityStreamsLeave https://www.w3.org/TR/activitystreams-vocabulary/#dfn-leave
|
||||||
ActivityStreamsLeave ActivityStreamsActivity = "Leave"
|
ActivityStreamsLeave = "Leave"
|
||||||
// ActivityStreamsLike https://www.w3.org/TR/activitystreams-vocabulary/#dfn-like
|
// ActivityStreamsLike https://www.w3.org/TR/activitystreams-vocabulary/#dfn-like
|
||||||
ActivityStreamsLike ActivityStreamsActivity = "Like"
|
ActivityStreamsLike = "Like"
|
||||||
// ActivityStreamsListen https://www.w3.org/TR/activitystreams-vocabulary/#dfn-listen
|
// ActivityStreamsListen https://www.w3.org/TR/activitystreams-vocabulary/#dfn-listen
|
||||||
ActivityStreamsListen ActivityStreamsActivity = "Listen"
|
ActivityStreamsListen = "Listen"
|
||||||
// ActivityStreamsMove https://www.w3.org/TR/activitystreams-vocabulary/#dfn-move
|
// ActivityStreamsMove https://www.w3.org/TR/activitystreams-vocabulary/#dfn-move
|
||||||
ActivityStreamsMove ActivityStreamsActivity = "Move"
|
ActivityStreamsMove = "Move"
|
||||||
// ActivityStreamsOffer https://www.w3.org/TR/activitystreams-vocabulary/#dfn-offer
|
// ActivityStreamsOffer https://www.w3.org/TR/activitystreams-vocabulary/#dfn-offer
|
||||||
ActivityStreamsOffer ActivityStreamsActivity = "Offer"
|
ActivityStreamsOffer = "Offer"
|
||||||
// ActivityStreamsQuestion https://www.w3.org/TR/activitystreams-vocabulary/#dfn-question
|
// ActivityStreamsQuestion https://www.w3.org/TR/activitystreams-vocabulary/#dfn-question
|
||||||
ActivityStreamsQuestion ActivityStreamsActivity = "Question"
|
ActivityStreamsQuestion = "Question"
|
||||||
// ActivityStreamsReject https://www.w3.org/TR/activitystreams-vocabulary/#dfn-reject
|
// ActivityStreamsReject https://www.w3.org/TR/activitystreams-vocabulary/#dfn-reject
|
||||||
ActivityStreamsReject ActivityStreamsActivity = "Reject"
|
ActivityStreamsReject = "Reject"
|
||||||
// ActivityStreamsRead https://www.w3.org/TR/activitystreams-vocabulary/#dfn-read
|
// ActivityStreamsRead https://www.w3.org/TR/activitystreams-vocabulary/#dfn-read
|
||||||
ActivityStreamsRead ActivityStreamsActivity = "Read"
|
ActivityStreamsRead = "Read"
|
||||||
// ActivityStreamsRemove https://www.w3.org/TR/activitystreams-vocabulary/#dfn-remove
|
// ActivityStreamsRemove https://www.w3.org/TR/activitystreams-vocabulary/#dfn-remove
|
||||||
ActivityStreamsRemove ActivityStreamsActivity = "Remove"
|
ActivityStreamsRemove = "Remove"
|
||||||
// ActivityStreamsTentativeReject https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tentativereject
|
// ActivityStreamsTentativeReject https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tentativereject
|
||||||
ActivityStreamsTentativeReject ActivityStreamsActivity = "TentativeReject"
|
ActivityStreamsTentativeReject = "TentativeReject"
|
||||||
// ActivityStreamsTentativeAccept https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tentativeaccept
|
// ActivityStreamsTentativeAccept https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tentativeaccept
|
||||||
ActivityStreamsTentativeAccept ActivityStreamsActivity = "TentativeAccept"
|
ActivityStreamsTentativeAccept = "TentativeAccept"
|
||||||
// ActivityStreamsTravel https://www.w3.org/TR/activitystreams-vocabulary/#dfn-travel
|
// ActivityStreamsTravel https://www.w3.org/TR/activitystreams-vocabulary/#dfn-travel
|
||||||
ActivityStreamsTravel ActivityStreamsActivity = "Travel"
|
ActivityStreamsTravel = "Travel"
|
||||||
// ActivityStreamsUndo https://www.w3.org/TR/activitystreams-vocabulary/#dfn-undo
|
// ActivityStreamsUndo https://www.w3.org/TR/activitystreams-vocabulary/#dfn-undo
|
||||||
ActivityStreamsUndo ActivityStreamsActivity = "Undo"
|
ActivityStreamsUndo = "Undo"
|
||||||
// ActivityStreamsUpdate https://www.w3.org/TR/activitystreams-vocabulary/#dfn-update
|
// ActivityStreamsUpdate https://www.w3.org/TR/activitystreams-vocabulary/#dfn-update
|
||||||
ActivityStreamsUpdate ActivityStreamsActivity = "Update"
|
ActivityStreamsUpdate = "Update"
|
||||||
// ActivityStreamsView https://www.w3.org/TR/activitystreams-vocabulary/#dfn-view
|
// ActivityStreamsView https://www.w3.org/TR/activitystreams-vocabulary/#dfn-view
|
||||||
ActivityStreamsView ActivityStreamsActivity = "View"
|
ActivityStreamsView = "View"
|
||||||
)
|
)
|
||||||
|
|
|
@ -38,6 +38,14 @@ type Mention struct {
|
||||||
TargetAccountID string `pg:",notnull"`
|
TargetAccountID string `pg:",notnull"`
|
||||||
// Prevent this mention from generating a notification?
|
// Prevent this mention from generating a notification?
|
||||||
Silent bool
|
Silent bool
|
||||||
|
|
||||||
|
/*
|
||||||
|
NON-DATABASE CONVENIENCE FIELDS
|
||||||
|
These fields are just for convenience while passing the mention
|
||||||
|
around internally, to make fewer database calls and whatnot. They're
|
||||||
|
not meant to be put in the database!
|
||||||
|
*/
|
||||||
|
|
||||||
// NameString is for putting in the namestring of the mentioned user
|
// NameString is for putting in the namestring of the mentioned user
|
||||||
// before the mention is dereferenced. Should be in a form along the lines of:
|
// before the mention is dereferenced. Should be in a form along the lines of:
|
||||||
// @whatever_username@example.org
|
// @whatever_username@example.org
|
||||||
|
@ -48,4 +56,6 @@ type Mention struct {
|
||||||
//
|
//
|
||||||
// This will not be put in the database, it's just for convenience.
|
// This will not be put in the database, it's just for convenience.
|
||||||
MentionedAccountURI string `pg:"-"`
|
MentionedAccountURI string `pg:"-"`
|
||||||
|
// A pointer to the gtsmodel account of the mentioned account.
|
||||||
|
GTSAccount *Account `pg:"-"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,11 @@ package gtsmodel
|
||||||
|
|
||||||
// FromClientAPI wraps a message that travels from client API into the processor
|
// FromClientAPI wraps a message that travels from client API into the processor
|
||||||
type FromClientAPI struct {
|
type FromClientAPI struct {
|
||||||
APObjectType ActivityStreamsObject
|
APObjectType string
|
||||||
APActivityType ActivityStreamsActivity
|
APActivityType string
|
||||||
GTSModel interface{}
|
GTSModel interface{}
|
||||||
|
OriginAccount *Account
|
||||||
|
TargetAccount *Account
|
||||||
}
|
}
|
||||||
|
|
||||||
// // ToFederator wraps a message that travels from the processor into the federator
|
// // ToFederator wraps a message that travels from the processor into the federator
|
||||||
|
@ -23,7 +25,8 @@ type FromClientAPI struct {
|
||||||
|
|
||||||
// FromFederator wraps a message that travels from the federator into the processor
|
// FromFederator wraps a message that travels from the federator into the processor
|
||||||
type FromFederator struct {
|
type FromFederator struct {
|
||||||
APObjectType ActivityStreamsObject
|
APObjectType string
|
||||||
APActivityType ActivityStreamsActivity
|
APActivityType string
|
||||||
GTSModel interface{}
|
GTSModel interface{}
|
||||||
|
ReceivingAccount *Account
|
||||||
}
|
}
|
||||||
|
|
49
internal/gtsmodel/relationship.go
Normal file
49
internal/gtsmodel/relationship.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 gtsmodel
|
||||||
|
|
||||||
|
// Relationship describes a requester's relationship with another account.
|
||||||
|
type Relationship struct {
|
||||||
|
// The account id.
|
||||||
|
ID string
|
||||||
|
// Are you following this user?
|
||||||
|
Following bool
|
||||||
|
// Are you receiving this user's boosts in your home timeline?
|
||||||
|
ShowingReblogs bool
|
||||||
|
// Have you enabled notifications for this user?
|
||||||
|
Notifying bool
|
||||||
|
// Are you followed by this user?
|
||||||
|
FollowedBy bool
|
||||||
|
// Are you blocking this user?
|
||||||
|
Blocking bool
|
||||||
|
// Is this user blocking you?
|
||||||
|
BlockedBy bool
|
||||||
|
// Are you muting this user?
|
||||||
|
Muting bool
|
||||||
|
// Are you muting notifications from this user?
|
||||||
|
MutingNotifications bool
|
||||||
|
// Do you have a pending follow request for this user?
|
||||||
|
Requested bool
|
||||||
|
// Are you blocking this user's domain?
|
||||||
|
DomainBlocking bool
|
||||||
|
// Are you featuring this user on your profile?
|
||||||
|
Endorsed bool
|
||||||
|
// Your note on this account.
|
||||||
|
Note string
|
||||||
|
}
|
|
@ -66,7 +66,7 @@ type Status struct {
|
||||||
VisibilityAdvanced *VisibilityAdvanced
|
VisibilityAdvanced *VisibilityAdvanced
|
||||||
// What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types
|
// What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types
|
||||||
// Will probably almost always be Note but who knows!.
|
// Will probably almost always be Note but who knows!.
|
||||||
ActivityStreamsType ActivityStreamsObject
|
ActivityStreamsType string
|
||||||
// Original text of the status without formatting
|
// Original text of the status without formatting
|
||||||
Text string
|
Text string
|
||||||
// Has this status been pinned by its owner?
|
// Has this status been pinned by its owner?
|
||||||
|
|
|
@ -67,7 +67,7 @@ type Handler interface {
|
||||||
// ProcessHeaderOrAvatar takes a new header image for an account, checks it out, removes exif data from it,
|
// ProcessHeaderOrAvatar takes a new header image for an account, checks it out, removes exif data from it,
|
||||||
// puts it in whatever storage backend we're using, sets the relevant fields in the database for the new image,
|
// puts it in whatever storage backend we're using, sets the relevant fields in the database for the new image,
|
||||||
// and then returns information to the caller about the new header.
|
// and then returns information to the caller about the new header.
|
||||||
ProcessHeaderOrAvatar(img []byte, accountID string, mediaType Type) (*gtsmodel.MediaAttachment, error)
|
ProcessHeaderOrAvatar(attachment []byte, accountID string, mediaType Type, remoteURL string) (*gtsmodel.MediaAttachment, error)
|
||||||
|
|
||||||
// ProcessLocalAttachment takes a new attachment and the requesting account, checks it out, removes exif data from it,
|
// ProcessLocalAttachment takes a new attachment and the requesting account, checks it out, removes exif data from it,
|
||||||
// puts it in whatever storage backend we're using, sets the relevant fields in the database for the new media,
|
// puts it in whatever storage backend we're using, sets the relevant fields in the database for the new media,
|
||||||
|
@ -86,6 +86,8 @@ type Handler interface {
|
||||||
// information to the caller about the new attachment. It's the caller's responsibility to put the returned struct
|
// information to the caller about the new attachment. It's the caller's responsibility to put the returned struct
|
||||||
// in the database.
|
// in the database.
|
||||||
ProcessRemoteAttachment(t transport.Transport, currentAttachment *gtsmodel.MediaAttachment, accountID string) (*gtsmodel.MediaAttachment, error)
|
ProcessRemoteAttachment(t transport.Transport, currentAttachment *gtsmodel.MediaAttachment, accountID string) (*gtsmodel.MediaAttachment, error)
|
||||||
|
|
||||||
|
ProcessRemoteHeaderOrAvatar(t transport.Transport, currentAttachment *gtsmodel.MediaAttachment, accountID string) (*gtsmodel.MediaAttachment, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type mediaHandler struct {
|
type mediaHandler struct {
|
||||||
|
@ -112,7 +114,7 @@ func New(config *config.Config, database db.DB, storage storage.Storage, log *lo
|
||||||
// ProcessHeaderOrAvatar takes a new header image for an account, checks it out, removes exif data from it,
|
// ProcessHeaderOrAvatar takes a new header image for an account, checks it out, removes exif data from it,
|
||||||
// puts it in whatever storage backend we're using, sets the relevant fields in the database for the new image,
|
// puts it in whatever storage backend we're using, sets the relevant fields in the database for the new image,
|
||||||
// and then returns information to the caller about the new header.
|
// and then returns information to the caller about the new header.
|
||||||
func (mh *mediaHandler) ProcessHeaderOrAvatar(attachment []byte, accountID string, mediaType Type) (*gtsmodel.MediaAttachment, error) {
|
func (mh *mediaHandler) ProcessHeaderOrAvatar(attachment []byte, accountID string, mediaType Type, remoteURL string) (*gtsmodel.MediaAttachment, error) {
|
||||||
l := mh.log.WithField("func", "SetHeaderForAccountID")
|
l := mh.log.WithField("func", "SetHeaderForAccountID")
|
||||||
|
|
||||||
if mediaType != Header && mediaType != Avatar {
|
if mediaType != Header && mediaType != Avatar {
|
||||||
|
@ -134,7 +136,7 @@ func (mh *mediaHandler) ProcessHeaderOrAvatar(attachment []byte, accountID strin
|
||||||
l.Tracef("read %d bytes of file", len(attachment))
|
l.Tracef("read %d bytes of file", len(attachment))
|
||||||
|
|
||||||
// process it
|
// process it
|
||||||
ma, err := mh.processHeaderOrAvi(attachment, contentType, mediaType, accountID)
|
ma, err := mh.processHeaderOrAvi(attachment, contentType, mediaType, accountID, remoteURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error processing %s: %s", mediaType, err)
|
return nil, fmt.Errorf("error processing %s: %s", mediaType, err)
|
||||||
}
|
}
|
||||||
|
@ -315,3 +317,43 @@ func (mh *mediaHandler) ProcessRemoteAttachment(t transport.Transport, currentAt
|
||||||
|
|
||||||
return mh.ProcessAttachment(attachmentBytes, accountID, currentAttachment.RemoteURL)
|
return mh.ProcessAttachment(attachmentBytes, accountID, currentAttachment.RemoteURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mh *mediaHandler) ProcessRemoteHeaderOrAvatar(t transport.Transport, currentAttachment *gtsmodel.MediaAttachment, accountID string) (*gtsmodel.MediaAttachment, error) {
|
||||||
|
|
||||||
|
if !currentAttachment.Header && !currentAttachment.Avatar {
|
||||||
|
return nil, errors.New("provided attachment was set to neither header nor avatar")
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentAttachment.Header && currentAttachment.Avatar {
|
||||||
|
return nil, errors.New("provided attachment was set to both header and avatar")
|
||||||
|
}
|
||||||
|
|
||||||
|
var headerOrAvi Type
|
||||||
|
if currentAttachment.Header {
|
||||||
|
headerOrAvi = Header
|
||||||
|
} else if currentAttachment.Avatar {
|
||||||
|
headerOrAvi = Avatar
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentAttachment.RemoteURL == "" {
|
||||||
|
return nil, errors.New("no remote URL on media attachment to dereference")
|
||||||
|
}
|
||||||
|
remoteIRI, err := url.Parse(currentAttachment.RemoteURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing attachment url %s: %s", currentAttachment.RemoteURL, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// for content type, we assume we don't know what to expect...
|
||||||
|
expectedContentType := "*/*"
|
||||||
|
if currentAttachment.File.ContentType != "" {
|
||||||
|
// ... and then narrow it down if we do
|
||||||
|
expectedContentType = currentAttachment.File.ContentType
|
||||||
|
}
|
||||||
|
|
||||||
|
attachmentBytes, err := t.DereferenceMedia(context.Background(), remoteIRI, expectedContentType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("dereferencing remote media with url %s: %s", remoteIRI.String(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mh.ProcessHeaderOrAvatar(attachmentBytes, accountID, headerOrAvi, currentAttachment.RemoteURL)
|
||||||
|
}
|
||||||
|
|
|
@ -147,7 +147,7 @@ func (suite *MediaTestSuite) TestSetHeaderOrAvatarForAccountID() {
|
||||||
f, err := ioutil.ReadFile("./test/test-jpeg.jpg")
|
f, err := ioutil.ReadFile("./test/test-jpeg.jpg")
|
||||||
assert.Nil(suite.T(), err)
|
assert.Nil(suite.T(), err)
|
||||||
|
|
||||||
ma, err := suite.mediaHandler.ProcessHeaderOrAvatar(f, "weeeeeee", "header")
|
ma, err := suite.mediaHandler.ProcessHeaderOrAvatar(f, "weeeeeee", "header", "")
|
||||||
assert.Nil(suite.T(), err)
|
assert.Nil(suite.T(), err)
|
||||||
suite.log.Debugf("%+v", ma)
|
suite.log.Debugf("%+v", ma)
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string, mediaType Type, accountID string) (*gtsmodel.MediaAttachment, error) {
|
func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string, mediaType Type, accountID string, remoteURL string) (*gtsmodel.MediaAttachment, error) {
|
||||||
var isHeader bool
|
var isHeader bool
|
||||||
var isAvatar bool
|
var isAvatar bool
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string
|
||||||
ID: newMediaID,
|
ID: newMediaID,
|
||||||
StatusID: "",
|
StatusID: "",
|
||||||
URL: originalURL,
|
URL: originalURL,
|
||||||
RemoteURL: "",
|
RemoteURL: remoteURL,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now(),
|
||||||
Type: gtsmodel.FileTypeImage,
|
Type: gtsmodel.FileTypeImage,
|
||||||
|
|
|
@ -78,6 +78,15 @@ func (p *processor) AccountGet(authed *oauth.Auth, targetAccountID string) (*api
|
||||||
return nil, fmt.Errorf("db error: %s", err)
|
return nil, fmt.Errorf("db error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lazily dereference things on the account if it hasn't been done yet
|
||||||
|
var requestingUsername string
|
||||||
|
if authed.Account != nil {
|
||||||
|
requestingUsername = authed.Account.Username
|
||||||
|
}
|
||||||
|
if err := p.dereferenceAccountFields(targetAccount, requestingUsername); err != nil {
|
||||||
|
p.log.WithField("func", "AccountGet").Debugf("dereferencing account: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
var mastoAccount *apimodel.Account
|
var mastoAccount *apimodel.Account
|
||||||
var err error
|
var err error
|
||||||
if authed.Account != nil && targetAccount.ID == authed.Account.ID {
|
if authed.Account != nil && targetAccount.ID == authed.Account.ID {
|
||||||
|
@ -285,6 +294,12 @@ func (p *processor) AccountFollowersGet(authed *oauth.Auth, targetAccountID stri
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// derefence account fields in case we haven't done it already
|
||||||
|
if err := p.dereferenceAccountFields(a, authed.Account.Username); err != nil {
|
||||||
|
// don't bail if we can't fetch them, we'll try another time
|
||||||
|
p.log.WithField("func", "AccountFollowersGet").Debugf("error dereferencing account fields: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
account, err := p.tc.AccountToMastoPublic(a)
|
account, err := p.tc.AccountToMastoPublic(a)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, NewErrorInternalError(err)
|
||||||
|
@ -293,3 +308,238 @@ func (p *processor) AccountFollowersGet(authed *oauth.Auth, targetAccountID stri
|
||||||
}
|
}
|
||||||
return accounts, nil
|
return accounts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *processor) AccountFollowingGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode) {
|
||||||
|
blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if blocked {
|
||||||
|
return nil, NewErrorNotFound(fmt.Errorf("block exists between accounts"))
|
||||||
|
}
|
||||||
|
|
||||||
|
following := []gtsmodel.Follow{}
|
||||||
|
accounts := []apimodel.Account{}
|
||||||
|
if err := p.db.GetFollowingByAccountID(targetAccountID, &following); err != nil {
|
||||||
|
if _, ok := err.(db.ErrNoEntries); ok {
|
||||||
|
return accounts, nil
|
||||||
|
}
|
||||||
|
return nil, NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range following {
|
||||||
|
blocked, err := p.db.Blocked(authed.Account.ID, f.AccountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
if blocked {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
a := >smodel.Account{}
|
||||||
|
if err := p.db.GetByID(f.TargetAccountID, a); err != nil {
|
||||||
|
if _, ok := err.(db.ErrNoEntries); ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// derefence account fields in case we haven't done it already
|
||||||
|
if err := p.dereferenceAccountFields(a, authed.Account.Username); err != nil {
|
||||||
|
// don't bail if we can't fetch them, we'll try another time
|
||||||
|
p.log.WithField("func", "AccountFollowingGet").Debugf("error dereferencing account fields: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := p.tc.AccountToMastoPublic(a)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
accounts = append(accounts, *account)
|
||||||
|
}
|
||||||
|
return accounts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *processor) AccountRelationshipGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode) {
|
||||||
|
if authed == nil || authed.Account == nil {
|
||||||
|
return nil, NewErrorForbidden(errors.New("not authed"))
|
||||||
|
}
|
||||||
|
|
||||||
|
gtsR, err := p.db.GetRelationship(authed.Account.ID, targetAccountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewErrorInternalError(fmt.Errorf("error getting relationship: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := p.tc.RelationshipToMasto(gtsR)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewErrorInternalError(fmt.Errorf("error converting relationship: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *processor) AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, ErrorWithCode) {
|
||||||
|
// if there's a block between the accounts we shouldn't create the request ofc
|
||||||
|
blocked, err := p.db.Blocked(authed.Account.ID, form.TargetAccountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
if blocked {
|
||||||
|
return nil, NewErrorNotFound(fmt.Errorf("accountfollowcreate: block exists between accounts"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure the target account actually exists in our db
|
||||||
|
targetAcct := >smodel.Account{}
|
||||||
|
if err := p.db.GetByID(form.TargetAccountID, targetAcct); err != nil {
|
||||||
|
if _, ok := err.(db.ErrNoEntries); ok {
|
||||||
|
return nil, NewErrorNotFound(fmt.Errorf("accountfollowcreate: account %s not found in the db: %s", form.TargetAccountID, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if a follow exists already
|
||||||
|
follows, err := p.db.Follows(authed.Account, targetAcct)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow in db: %s", err))
|
||||||
|
}
|
||||||
|
if follows {
|
||||||
|
// already follows so just return the relationship
|
||||||
|
return p.AccountRelationshipGet(authed, form.TargetAccountID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if a follow exists already
|
||||||
|
followRequested, err := p.db.FollowRequested(authed.Account, targetAcct)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow request in db: %s", err))
|
||||||
|
}
|
||||||
|
if followRequested {
|
||||||
|
// already follow requested so just return the relationship
|
||||||
|
return p.AccountRelationshipGet(authed, form.TargetAccountID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// make the follow request
|
||||||
|
fr := >smodel.FollowRequest{
|
||||||
|
AccountID: authed.Account.ID,
|
||||||
|
TargetAccountID: form.TargetAccountID,
|
||||||
|
ShowReblogs: true,
|
||||||
|
URI: util.GenerateURIForFollow(authed.Account.Username, p.config.Protocol, p.config.Host),
|
||||||
|
Notify: false,
|
||||||
|
}
|
||||||
|
if form.Reblogs != nil {
|
||||||
|
fr.ShowReblogs = *form.Reblogs
|
||||||
|
}
|
||||||
|
if form.Notify != nil {
|
||||||
|
fr.Notify = *form.Notify
|
||||||
|
}
|
||||||
|
|
||||||
|
// whack it in the database
|
||||||
|
if err := p.db.Put(fr); err != nil {
|
||||||
|
return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error creating follow request in db: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it's a local account that's not locked we can just straight up accept the follow request
|
||||||
|
if !targetAcct.Locked && targetAcct.Domain == "" {
|
||||||
|
if _, err := p.db.AcceptFollowRequest(authed.Account.ID, form.TargetAccountID); err != nil {
|
||||||
|
return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error accepting folow request for local unlocked account: %s", err))
|
||||||
|
}
|
||||||
|
// return the new relationship
|
||||||
|
return p.AccountRelationshipGet(authed, form.TargetAccountID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise we leave the follow request as it is and we handle the rest of the process asynchronously
|
||||||
|
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
||||||
|
APObjectType: gtsmodel.ActivityStreamsFollow,
|
||||||
|
APActivityType: gtsmodel.ActivityStreamsCreate,
|
||||||
|
GTSModel: >smodel.Follow{
|
||||||
|
AccountID: authed.Account.ID,
|
||||||
|
TargetAccountID: form.TargetAccountID,
|
||||||
|
URI: fr.URI,
|
||||||
|
},
|
||||||
|
OriginAccount: authed.Account,
|
||||||
|
TargetAccount: targetAcct,
|
||||||
|
}
|
||||||
|
|
||||||
|
// return whatever relationship results from this
|
||||||
|
return p.AccountRelationshipGet(authed, form.TargetAccountID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *processor) AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode) {
|
||||||
|
// if there's a block between the accounts we shouldn't do anything
|
||||||
|
blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
if blocked {
|
||||||
|
return nil, NewErrorNotFound(fmt.Errorf("AccountFollowRemove: block exists between accounts"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, NewErrorNotFound(fmt.Errorf("AccountFollowRemove: account %s not found in the db: %s", targetAccountID, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if a follow request exists, 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: authed.Account.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, NewErrorInternalError(fmt.Errorf("AccountFollowRemove: 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: authed.Account.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, NewErrorInternalError(fmt.Errorf("AccountFollowRemove: 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: authed.Account.ID,
|
||||||
|
TargetAccountID: targetAccountID,
|
||||||
|
URI: frURI,
|
||||||
|
},
|
||||||
|
OriginAccount: authed.Account,
|
||||||
|
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: authed.Account.ID,
|
||||||
|
TargetAccountID: targetAccountID,
|
||||||
|
URI: fURI,
|
||||||
|
},
|
||||||
|
OriginAccount: authed.Account,
|
||||||
|
TargetAccount: targetAcct,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return whatever relationship results from all this
|
||||||
|
return p.AccountRelationshipGet(authed, targetAccountID)
|
||||||
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"github.com/go-fed/activity/streams"
|
"github.com/go-fed/activity/streams"
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
@ -46,7 +47,7 @@ func (p *processor) authenticateAndDereferenceFediRequest(username string, r *ht
|
||||||
// we might already have an entry for this account so check that first
|
// we might already have an entry for this account so check that first
|
||||||
requestingAccount := >smodel.Account{}
|
requestingAccount := >smodel.Account{}
|
||||||
|
|
||||||
err = p.db.GetWhere("uri", requestingAccountURI.String(), requestingAccount)
|
err = p.db.GetWhere([]db.Where{{Key: "uri", Value: requestingAccountURI.String()}}, requestingAccount)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// we do have it yay, return it
|
// we do have it yay, return it
|
||||||
return requestingAccount, nil
|
return requestingAccount, nil
|
||||||
|
@ -122,6 +123,89 @@ func (p *processor) GetFediUser(requestedUsername string, request *http.Request)
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *processor) GetFediFollowers(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) {
|
||||||
|
// get the account the request is referring to
|
||||||
|
requestedAccount := >smodel.Account{}
|
||||||
|
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
|
||||||
|
return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// authenticate the request
|
||||||
|
requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewErrorNotAuthorized(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if blocked {
|
||||||
|
return nil, NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
requestedAccountURI, err := url.Parse(requestedAccount.URI)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
requestedFollowers, err := p.federator.FederatingDB().Followers(context.Background(), requestedAccountURI)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewErrorInternalError(fmt.Errorf("error fetching followers for uri %s: %s", requestedAccountURI.String(), err))
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := streams.Serialize(requestedFollowers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, ErrorWithCode) {
|
||||||
|
// get the account the request is referring to
|
||||||
|
requestedAccount := >smodel.Account{}
|
||||||
|
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
|
||||||
|
return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// authenticate the request
|
||||||
|
requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewErrorNotAuthorized(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if blocked {
|
||||||
|
return nil, NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
s := >smodel.Status{}
|
||||||
|
if err := p.db.GetWhere([]db.Where{
|
||||||
|
{Key: "id", Value: requestedStatusID},
|
||||||
|
{Key: "account_id", Value: requestedAccount.ID},
|
||||||
|
}, s); err != nil {
|
||||||
|
return nil, NewErrorNotFound(fmt.Errorf("database error getting status with id %s and account id %s: %s", requestedStatusID, requestedAccount.ID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
asStatus, err := p.tc.StatusToAS(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := streams.Serialize(asStatus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *processor) GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, ErrorWithCode) {
|
func (p *processor) GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, ErrorWithCode) {
|
||||||
// get the account the request is referring to
|
// get the account the request is referring to
|
||||||
requestedAccount := >smodel.Account{}
|
requestedAccount := >smodel.Account{}
|
||||||
|
|
|
@ -19,55 +19,198 @@
|
||||||
package message
|
package message
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/go-fed/activity/streams"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error {
|
func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error {
|
||||||
switch clientMsg.APObjectType {
|
switch clientMsg.APActivityType {
|
||||||
case gtsmodel.ActivityStreamsNote:
|
case gtsmodel.ActivityStreamsCreate:
|
||||||
status, ok := clientMsg.GTSModel.(*gtsmodel.Status)
|
// CREATE
|
||||||
if !ok {
|
switch clientMsg.APObjectType {
|
||||||
return errors.New("note was not parseable as *gtsmodel.Status")
|
case gtsmodel.ActivityStreamsNote:
|
||||||
}
|
// CREATE NOTE
|
||||||
|
status, ok := clientMsg.GTSModel.(*gtsmodel.Status)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("note was not parseable as *gtsmodel.Status")
|
||||||
|
}
|
||||||
|
|
||||||
if err := p.notifyStatus(status); err != nil {
|
if err := p.notifyStatus(status); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if status.VisibilityAdvanced.Federated {
|
if status.VisibilityAdvanced.Federated {
|
||||||
return p.federateStatus(status)
|
return p.federateStatus(status)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case gtsmodel.ActivityStreamsFollow:
|
||||||
|
// CREATE FOLLOW (request)
|
||||||
|
follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("follow was not parseable as *gtsmodel.Follow")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.notifyFollow(follow); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.federateFollow(follow, clientMsg.OriginAccount, clientMsg.TargetAccount)
|
||||||
|
}
|
||||||
|
case gtsmodel.ActivityStreamsUpdate:
|
||||||
|
// UPDATE
|
||||||
|
case gtsmodel.ActivityStreamsAccept:
|
||||||
|
// ACCEPT
|
||||||
|
switch clientMsg.APObjectType {
|
||||||
|
case gtsmodel.ActivityStreamsFollow:
|
||||||
|
// ACCEPT FOLLOW
|
||||||
|
follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("accept was not parseable as *gtsmodel.Follow")
|
||||||
|
}
|
||||||
|
return p.federateAcceptFollowRequest(follow, clientMsg.OriginAccount, clientMsg.TargetAccount)
|
||||||
|
}
|
||||||
|
case gtsmodel.ActivityStreamsUndo:
|
||||||
|
// UNDO
|
||||||
|
switch clientMsg.APObjectType {
|
||||||
|
case gtsmodel.ActivityStreamsFollow:
|
||||||
|
// UNDO FOLLOW
|
||||||
|
follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("undo was not parseable as *gtsmodel.Follow")
|
||||||
|
}
|
||||||
|
return p.federateUnfollow(follow, clientMsg.OriginAccount, clientMsg.TargetAccount)
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
return fmt.Errorf("message type unprocessable: %+v", clientMsg)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) federateStatus(status *gtsmodel.Status) error {
|
func (p *processor) federateStatus(status *gtsmodel.Status) error {
|
||||||
// // derive the sending account -- it might be attached to the status already
|
asStatus, err := p.tc.StatusToAS(status)
|
||||||
// sendingAcct := >smodel.Account{}
|
if err != nil {
|
||||||
// if status.GTSAccount != nil {
|
return fmt.Errorf("federateStatus: error converting status to as format: %s", err)
|
||||||
// sendingAcct = status.GTSAccount
|
}
|
||||||
// } else {
|
|
||||||
// // it wasn't attached so get it from the db instead
|
|
||||||
// if err := p.db.GetByID(status.AccountID, sendingAcct); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// outboxURI, err := url.Parse(sendingAcct.OutboxURI)
|
outboxIRI, err := url.Parse(status.GTSAccount.OutboxURI)
|
||||||
// if err != nil {
|
if err != nil {
|
||||||
// return err
|
return fmt.Errorf("federateStatus: error parsing outboxURI %s: %s", status.GTSAccount.OutboxURI, err)
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // convert the status to AS format Note
|
_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, asStatus)
|
||||||
// note, err := p.tc.StatusToAS(status)
|
return err
|
||||||
// if err != nil {
|
}
|
||||||
// return err
|
|
||||||
// }
|
func (p *processor) federateFollow(follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
|
||||||
|
// if both accounts are local there's nothing to do here
|
||||||
// _, err = p.federator.FederatingActor().Send(context.Background(), outboxURI, note)
|
if originAccount.Domain == "" && targetAccount.Domain == "" {
|
||||||
return nil
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
asFollow, err := p.tc.FollowToAS(follow, originAccount, targetAccount)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("federateFollow: error converting follow to as format: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
outboxIRI, err := url.Parse(originAccount.OutboxURI)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("federateFollow: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, asFollow)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *processor) federateUnfollow(follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
|
||||||
|
// if both accounts are local there's nothing to do here
|
||||||
|
if originAccount.Domain == "" && targetAccount.Domain == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// recreate the follow
|
||||||
|
asFollow, err := p.tc.FollowToAS(follow, originAccount, targetAccount)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("federateUnfollow: error converting follow to as format: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
targetAccountURI, err := url.Parse(targetAccount.URI)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing uri %s: %s", targetAccount.URI, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create an Undo and set the appropriate actor on it
|
||||||
|
undo := streams.NewActivityStreamsUndo()
|
||||||
|
undo.SetActivityStreamsActor(asFollow.GetActivityStreamsActor())
|
||||||
|
|
||||||
|
// Set the recreated follow as the 'object' property.
|
||||||
|
undoObject := streams.NewActivityStreamsObjectProperty()
|
||||||
|
undoObject.AppendActivityStreamsFollow(asFollow)
|
||||||
|
undo.SetActivityStreamsObject(undoObject)
|
||||||
|
|
||||||
|
// Set the To of the undo as the target of the recreated follow
|
||||||
|
undoTo := streams.NewActivityStreamsToProperty()
|
||||||
|
undoTo.AppendIRI(targetAccountURI)
|
||||||
|
undo.SetActivityStreamsTo(undoTo)
|
||||||
|
|
||||||
|
outboxIRI, err := url.Parse(originAccount.OutboxURI)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("federateUnfollow: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// send off the Undo
|
||||||
|
_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, undo)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *processor) federateAcceptFollowRequest(follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
|
||||||
|
// if both accounts are local there's nothing to do here
|
||||||
|
if originAccount.Domain == "" && targetAccount.Domain == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// recreate the AS follow
|
||||||
|
asFollow, err := p.tc.FollowToAS(follow, originAccount, targetAccount)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("federateUnfollow: error converting follow to as format: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
acceptingAccountURI, err := url.Parse(targetAccount.URI)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing uri %s: %s", targetAccount.URI, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
requestingAccountURI, err := url.Parse(originAccount.URI)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing uri %s: %s", targetAccount.URI, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create an Accept
|
||||||
|
accept := streams.NewActivityStreamsAccept()
|
||||||
|
|
||||||
|
// set the accepting actor on it
|
||||||
|
acceptActorProp := streams.NewActivityStreamsActorProperty()
|
||||||
|
acceptActorProp.AppendIRI(acceptingAccountURI)
|
||||||
|
accept.SetActivityStreamsActor(acceptActorProp)
|
||||||
|
|
||||||
|
// Set the recreated follow as the 'object' property.
|
||||||
|
acceptObject := streams.NewActivityStreamsObjectProperty()
|
||||||
|
acceptObject.AppendActivityStreamsFollow(asFollow)
|
||||||
|
accept.SetActivityStreamsObject(acceptObject)
|
||||||
|
|
||||||
|
// Set the To of the accept as the originator of the follow
|
||||||
|
acceptTo := streams.NewActivityStreamsToProperty()
|
||||||
|
acceptTo.AppendIRI(requestingAccountURI)
|
||||||
|
accept.SetActivityStreamsTo(acceptTo)
|
||||||
|
|
||||||
|
outboxIRI, err := url.Parse(targetAccount.OutboxURI)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("federateAcceptFollowRequest: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// send off the accept using the accepter's outbox
|
||||||
|
_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, accept)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,3 +23,7 @@ import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
func (p *processor) notifyStatus(status *gtsmodel.Status) error {
|
func (p *processor) notifyStatus(status *gtsmodel.Status) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *processor) notifyFollow(follow *gtsmodel.Follow) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -38,24 +38,60 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
|
||||||
|
|
||||||
l.Debug("entering function PROCESS FROM FEDERATOR")
|
l.Debug("entering function PROCESS FROM FEDERATOR")
|
||||||
|
|
||||||
switch federatorMsg.APObjectType {
|
switch federatorMsg.APActivityType {
|
||||||
case gtsmodel.ActivityStreamsNote:
|
case gtsmodel.ActivityStreamsCreate:
|
||||||
|
// CREATE
|
||||||
|
switch federatorMsg.APObjectType {
|
||||||
|
case gtsmodel.ActivityStreamsNote:
|
||||||
|
// CREATE A STATUS
|
||||||
|
incomingStatus, ok := federatorMsg.GTSModel.(*gtsmodel.Status)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("note was not parseable as *gtsmodel.Status")
|
||||||
|
}
|
||||||
|
|
||||||
incomingStatus, ok := federatorMsg.GTSModel.(*gtsmodel.Status)
|
l.Debug("will now derefence incoming status")
|
||||||
if !ok {
|
if err := p.dereferenceStatusFields(incomingStatus); err != nil {
|
||||||
return errors.New("note was not parseable as *gtsmodel.Status")
|
return fmt.Errorf("error dereferencing status from federator: %s", err)
|
||||||
}
|
}
|
||||||
|
if err := p.db.UpdateByID(incomingStatus.ID, incomingStatus); err != nil {
|
||||||
|
return fmt.Errorf("error updating dereferenced status in the db: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
l.Debug("will now derefence incoming status")
|
if err := p.notifyStatus(incomingStatus); err != nil {
|
||||||
if err := p.dereferenceStatusFields(incomingStatus); err != nil {
|
return err
|
||||||
return fmt.Errorf("error dereferencing status from federator: %s", err)
|
}
|
||||||
}
|
case gtsmodel.ActivityStreamsProfile:
|
||||||
if err := p.db.UpdateByID(incomingStatus.ID, incomingStatus); err != nil {
|
// CREATE AN ACCOUNT
|
||||||
return fmt.Errorf("error updating dereferenced status in the db: %s", err)
|
incomingAccount, ok := federatorMsg.GTSModel.(*gtsmodel.Account)
|
||||||
}
|
if !ok {
|
||||||
|
return errors.New("profile was not parseable as *gtsmodel.Account")
|
||||||
|
}
|
||||||
|
|
||||||
if err := p.notifyStatus(incomingStatus); err != nil {
|
l.Debug("will now derefence incoming account")
|
||||||
return err
|
if err := p.dereferenceAccountFields(incomingAccount, ""); err != nil {
|
||||||
|
return fmt.Errorf("error dereferencing account from federator: %s", err)
|
||||||
|
}
|
||||||
|
if err := p.db.UpdateByID(incomingAccount.ID, incomingAccount); err != nil {
|
||||||
|
return fmt.Errorf("error updating dereferenced account in the db: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case gtsmodel.ActivityStreamsUpdate:
|
||||||
|
// UPDATE
|
||||||
|
switch federatorMsg.APObjectType {
|
||||||
|
case gtsmodel.ActivityStreamsProfile:
|
||||||
|
// UPDATE AN ACCOUNT
|
||||||
|
incomingAccount, ok := federatorMsg.GTSModel.(*gtsmodel.Account)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("profile was not parseable as *gtsmodel.Account")
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Debug("will now derefence incoming account")
|
||||||
|
if err := p.dereferenceAccountFields(incomingAccount, ""); err != nil {
|
||||||
|
return fmt.Errorf("error dereferencing account from federator: %s", err)
|
||||||
|
}
|
||||||
|
if err := p.db.UpdateByID(incomingAccount.ID, incomingAccount); err != nil {
|
||||||
|
return fmt.Errorf("error updating dereferenced account in the db: %s", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,7 +157,7 @@ func (p *processor) dereferenceStatusFields(status *gtsmodel.Status) error {
|
||||||
|
|
||||||
// it might have been processed elsewhere so check first if it's already in the database or not
|
// it might have been processed elsewhere so check first if it's already in the database or not
|
||||||
maybeAttachment := >smodel.MediaAttachment{}
|
maybeAttachment := >smodel.MediaAttachment{}
|
||||||
err := p.db.GetWhere("remote_url", a.RemoteURL, maybeAttachment)
|
err := p.db.GetWhere([]db.Where{{Key: "remote_url", Value: a.RemoteURL}}, maybeAttachment)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// we already have it in the db, dereferenced, no need to do it again
|
// we already have it in the db, dereferenced, no need to do it again
|
||||||
l.Debugf("attachment already exists with id %s", maybeAttachment.ID)
|
l.Debugf("attachment already exists with id %s", maybeAttachment.ID)
|
||||||
|
@ -170,7 +206,7 @@ func (p *processor) dereferenceStatusFields(status *gtsmodel.Status) error {
|
||||||
m.OriginAccountURI = status.GTSAccount.URI
|
m.OriginAccountURI = status.GTSAccount.URI
|
||||||
|
|
||||||
targetAccount := >smodel.Account{}
|
targetAccount := >smodel.Account{}
|
||||||
if err := p.db.GetWhere("uri", uri.String(), targetAccount); err != nil {
|
if err := p.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, targetAccount); err != nil {
|
||||||
// proper error
|
// proper error
|
||||||
if _, ok := err.(db.ErrNoEntries); !ok {
|
if _, ok := err.(db.ErrNoEntries); !ok {
|
||||||
return fmt.Errorf("db error checking for account with uri %s", uri.String())
|
return fmt.Errorf("db error checking for account with uri %s", uri.String())
|
||||||
|
@ -206,3 +242,27 @@ func (p *processor) dereferenceStatusFields(status *gtsmodel.Status) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *processor) dereferenceAccountFields(account *gtsmodel.Account, requestingUsername string) error {
|
||||||
|
l := p.log.WithFields(logrus.Fields{
|
||||||
|
"func": "dereferenceAccountFields",
|
||||||
|
"requestingUsername": requestingUsername,
|
||||||
|
})
|
||||||
|
|
||||||
|
t, err := p.federator.GetTransportForUser(requestingUsername)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error getting transport for user: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch the header and avatar
|
||||||
|
if err := p.fetchHeaderAndAviForAccount(account, t); err != nil {
|
||||||
|
// if this doesn't work, just skip it -- we can do it later
|
||||||
|
l.Debugf("error fetching header/avi for account: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.db.UpdateByID(account.ID, account); err != nil {
|
||||||
|
return fmt.Errorf("error updating account in database: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -48,11 +48,28 @@ func (p *processor) FollowRequestsGet(auth *oauth.Auth) ([]apimodel.Account, Err
|
||||||
return accts, nil
|
return accts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) FollowRequestAccept(auth *oauth.Auth, accountID string) ErrorWithCode {
|
func (p *processor) FollowRequestAccept(auth *oauth.Auth, accountID string) (*apimodel.Relationship, ErrorWithCode) {
|
||||||
if err := p.db.AcceptFollowRequest(accountID, auth.Account.ID); err != nil {
|
follow, err := p.db.AcceptFollowRequest(accountID, auth.Account.ID)
|
||||||
return NewErrorNotFound(err)
|
if err != nil {
|
||||||
|
return nil, NewErrorNotFound(err)
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
||||||
|
APActivityType: gtsmodel.ActivityStreamsAccept,
|
||||||
|
GTSModel: follow,
|
||||||
|
}
|
||||||
|
|
||||||
|
gtsR, err := p.db.GetRelationship(auth.Account.ID, accountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := p.tc.RelationshipToMasto(gtsR)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) FollowRequestDeny(auth *oauth.Auth) ErrorWithCode {
|
func (p *processor) FollowRequestDeny(auth *oauth.Auth) ErrorWithCode {
|
||||||
|
|
|
@ -22,12 +22,13 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *processor) InstanceGet(domain string) (*apimodel.Instance, ErrorWithCode) {
|
func (p *processor) InstanceGet(domain string) (*apimodel.Instance, ErrorWithCode) {
|
||||||
i := >smodel.Instance{}
|
i := >smodel.Instance{}
|
||||||
if err := p.db.GetWhere("domain", domain, i); err != nil {
|
if err := p.db.GetWhere([]db.Where{{Key: "domain", Value: domain}}, i); err != nil {
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("db error fetching instance %s: %s", p.config.Host, err))
|
return nil, NewErrorInternalError(fmt.Errorf("db error fetching instance %s: %s", p.config.Host, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -71,8 +71,16 @@ type Processor interface {
|
||||||
// AccountStatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for
|
// AccountStatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for
|
||||||
// the account given in authed.
|
// the account given in authed.
|
||||||
AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, ErrorWithCode)
|
AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, ErrorWithCode)
|
||||||
// AccountFollowersGet
|
// AccountFollowersGet fetches a list of the target account's followers.
|
||||||
AccountFollowersGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode)
|
AccountFollowersGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode)
|
||||||
|
// AccountFollowingGet fetches a list of the accounts that target account is following.
|
||||||
|
AccountFollowingGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode)
|
||||||
|
// AccountRelationshipGet returns a relationship model describing the relationship of the targetAccount to the Authed account.
|
||||||
|
AccountRelationshipGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode)
|
||||||
|
// AccountFollowCreate handles a follow request to an account, either remote or local.
|
||||||
|
AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, ErrorWithCode)
|
||||||
|
// AccountFollowRemove handles the removal of a follow/follow request to an account, either remote or local.
|
||||||
|
AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode)
|
||||||
|
|
||||||
// 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)
|
||||||
|
@ -86,7 +94,7 @@ type Processor interface {
|
||||||
// FollowRequestsGet handles the getting of the authed account's incoming follow requests
|
// FollowRequestsGet handles the getting of the authed account's incoming follow requests
|
||||||
FollowRequestsGet(auth *oauth.Auth) ([]apimodel.Account, ErrorWithCode)
|
FollowRequestsGet(auth *oauth.Auth) ([]apimodel.Account, ErrorWithCode)
|
||||||
// FollowRequestAccept handles the acceptance of a follow request from the given account ID
|
// FollowRequestAccept handles the acceptance of a follow request from the given account ID
|
||||||
FollowRequestAccept(auth *oauth.Auth, accountID string) ErrorWithCode
|
FollowRequestAccept(auth *oauth.Auth, accountID string) (*apimodel.Relationship, ErrorWithCode)
|
||||||
|
|
||||||
// InstanceGet retrieves instance information for serving at api/v1/instance
|
// InstanceGet retrieves instance information for serving at api/v1/instance
|
||||||
InstanceGet(domain string) (*apimodel.Instance, ErrorWithCode)
|
InstanceGet(domain string) (*apimodel.Instance, ErrorWithCode)
|
||||||
|
@ -125,6 +133,14 @@ type Processor interface {
|
||||||
// before returning a JSON serializable interface to the caller.
|
// before returning a JSON serializable interface to the caller.
|
||||||
GetFediUser(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode)
|
GetFediUser(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode)
|
||||||
|
|
||||||
|
// GetFediFollowers handles the getting of a fedi/activitypub representation of a user/account's followers, performing appropriate
|
||||||
|
// authentication before returning a JSON serializable interface to the caller.
|
||||||
|
GetFediFollowers(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode)
|
||||||
|
|
||||||
|
// GetFediStatus handles the getting of a fedi/activitypub representation of a particular status, performing appropriate
|
||||||
|
// authentication before returning a JSON serializable interface to the caller.
|
||||||
|
GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, ErrorWithCode)
|
||||||
|
|
||||||
// GetWebfingerAccount handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups.
|
// GetWebfingerAccount handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups.
|
||||||
GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, ErrorWithCode)
|
GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, ErrorWithCode)
|
||||||
|
|
||||||
|
@ -200,15 +216,11 @@ func (p *processor) Start() error {
|
||||||
DistLoop:
|
DistLoop:
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
// case clientMsg := <-p.toClientAPI:
|
|
||||||
// p.log.Infof("received message TO client API: %+v", clientMsg)
|
|
||||||
case clientMsg := <-p.fromClientAPI:
|
case clientMsg := <-p.fromClientAPI:
|
||||||
p.log.Infof("received message FROM client API: %+v", clientMsg)
|
p.log.Infof("received message FROM client API: %+v", clientMsg)
|
||||||
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.toFederator:
|
|
||||||
// p.log.Infof("received message TO federator: %+v", federatorMsg)
|
|
||||||
case federatorMsg := <-p.fromFederator:
|
case federatorMsg := <-p.fromFederator:
|
||||||
p.log.Infof("received message FROM federator: %+v", federatorMsg)
|
p.log.Infof("received message FROM federator: %+v", federatorMsg)
|
||||||
if err := p.processFromFederator(federatorMsg); err != nil {
|
if err := p.processFromFederator(federatorMsg); err != nil {
|
||||||
|
|
|
@ -29,6 +29,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -205,7 +206,7 @@ func (p *processor) processMentions(form *apimodel.AdvancedStatusCreateForm, acc
|
||||||
if err := p.db.Put(menchie); err != nil {
|
if err := p.db.Put(menchie); err != nil {
|
||||||
return fmt.Errorf("error putting mentions in db: %s", err)
|
return fmt.Errorf("error putting mentions in db: %s", err)
|
||||||
}
|
}
|
||||||
menchies = append(menchies, menchie.TargetAccountID)
|
menchies = append(menchies, menchie.ID)
|
||||||
}
|
}
|
||||||
// add full populated gts menchies to the status for passing them around conveniently
|
// add full populated gts menchies to the status for passing them around conveniently
|
||||||
status.GTSMentions = gtsMenchies
|
status.GTSMentions = gtsMenchies
|
||||||
|
@ -280,7 +281,7 @@ func (p *processor) updateAccountAvatar(avatar *multipart.FileHeader, accountID
|
||||||
}
|
}
|
||||||
|
|
||||||
// do the setting
|
// do the setting
|
||||||
avatarInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Avatar)
|
avatarInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Avatar, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error processing avatar: %s", err)
|
return nil, fmt.Errorf("error processing avatar: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -313,10 +314,42 @@ func (p *processor) updateAccountHeader(header *multipart.FileHeader, accountID
|
||||||
}
|
}
|
||||||
|
|
||||||
// do the setting
|
// do the setting
|
||||||
headerInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Header)
|
headerInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Header, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error processing header: %s", err)
|
return nil, fmt.Errorf("error processing header: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return headerInfo, f.Close()
|
return headerInfo, f.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fetchHeaderAndAviForAccount fetches the header and avatar for a remote account, using a transport
|
||||||
|
// on behalf of requestingUsername.
|
||||||
|
//
|
||||||
|
// targetAccount's AvatarMediaAttachmentID and HeaderMediaAttachmentID will be updated as necessary.
|
||||||
|
//
|
||||||
|
// SIDE EFFECTS: remote header and avatar will be stored in local storage, and the database will be updated
|
||||||
|
// to reflect the creation of these new attachments.
|
||||||
|
func (p *processor) fetchHeaderAndAviForAccount(targetAccount *gtsmodel.Account, t transport.Transport) error {
|
||||||
|
if targetAccount.AvatarRemoteURL != "" && targetAccount.AvatarMediaAttachmentID == "" {
|
||||||
|
a, err := p.mediaHandler.ProcessRemoteHeaderOrAvatar(t, >smodel.MediaAttachment{
|
||||||
|
RemoteURL: targetAccount.AvatarRemoteURL,
|
||||||
|
Avatar: true,
|
||||||
|
}, targetAccount.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error processing avatar for user: %s", err)
|
||||||
|
}
|
||||||
|
targetAccount.AvatarMediaAttachmentID = a.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetAccount.HeaderRemoteURL != "" && targetAccount.HeaderMediaAttachmentID == "" {
|
||||||
|
a, err := p.mediaHandler.ProcessRemoteHeaderOrAvatar(t, >smodel.MediaAttachment{
|
||||||
|
RemoteURL: targetAccount.HeaderRemoteURL,
|
||||||
|
Header: true,
|
||||||
|
}, targetAccount.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error processing header for user: %s", err)
|
||||||
|
}
|
||||||
|
targetAccount.HeaderMediaAttachmentID = a.ID
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -106,17 +106,17 @@ func (pts *tokenStore) Create(ctx context.Context, info oauth2.TokenInfo) error
|
||||||
|
|
||||||
// RemoveByCode deletes a token from the DB based on the Code field
|
// RemoveByCode deletes a token from the DB based on the Code field
|
||||||
func (pts *tokenStore) RemoveByCode(ctx context.Context, code string) error {
|
func (pts *tokenStore) RemoveByCode(ctx context.Context, code string) error {
|
||||||
return pts.db.DeleteWhere("code", code, &Token{})
|
return pts.db.DeleteWhere([]db.Where{{Key: "code", Value: code}}, &Token{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveByAccess deletes a token from the DB based on the Access field
|
// RemoveByAccess deletes a token from the DB based on the Access field
|
||||||
func (pts *tokenStore) RemoveByAccess(ctx context.Context, access string) error {
|
func (pts *tokenStore) RemoveByAccess(ctx context.Context, access string) error {
|
||||||
return pts.db.DeleteWhere("access", access, &Token{})
|
return pts.db.DeleteWhere([]db.Where{{Key: "access", Value: access}}, &Token{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveByRefresh deletes a token from the DB based on the Refresh field
|
// RemoveByRefresh deletes a token from the DB based on the Refresh field
|
||||||
func (pts *tokenStore) RemoveByRefresh(ctx context.Context, refresh string) error {
|
func (pts *tokenStore) RemoveByRefresh(ctx context.Context, refresh string) error {
|
||||||
return pts.db.DeleteWhere("refresh", refresh, &Token{})
|
return pts.db.DeleteWhere([]db.Where{{Key: "refresh", Value: refresh}}, &Token{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetByCode selects a token from the DB based on the Code field
|
// GetByCode selects a token from the DB based on the Code field
|
||||||
|
@ -127,7 +127,7 @@ func (pts *tokenStore) GetByCode(ctx context.Context, code string) (oauth2.Token
|
||||||
pgt := &Token{
|
pgt := &Token{
|
||||||
Code: code,
|
Code: code,
|
||||||
}
|
}
|
||||||
if err := pts.db.GetWhere("code", code, pgt); err != nil {
|
if err := pts.db.GetWhere([]db.Where{{Key: "code", Value: code}}, pgt); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return TokenToOauthToken(pgt), nil
|
return TokenToOauthToken(pgt), nil
|
||||||
|
@ -141,7 +141,7 @@ func (pts *tokenStore) GetByAccess(ctx context.Context, access string) (oauth2.T
|
||||||
pgt := &Token{
|
pgt := &Token{
|
||||||
Access: access,
|
Access: access,
|
||||||
}
|
}
|
||||||
if err := pts.db.GetWhere("access", access, pgt); err != nil {
|
if err := pts.db.GetWhere([]db.Where{{Key: "access", Value: access}}, pgt); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return TokenToOauthToken(pgt), nil
|
return TokenToOauthToken(pgt), nil
|
||||||
|
@ -155,7 +155,7 @@ func (pts *tokenStore) GetByRefresh(ctx context.Context, refresh string) (oauth2
|
||||||
pgt := &Token{
|
pgt := &Token{
|
||||||
Refresh: refresh,
|
Refresh: refresh,
|
||||||
}
|
}
|
||||||
if err := pts.db.GetWhere("refresh", refresh, pgt); err != nil {
|
if err := pts.db.GetWhere([]db.Where{{Key: "refresh", Value: refresh}}, pgt); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return TokenToOauthToken(pgt), nil
|
return TokenToOauthToken(pgt), nil
|
||||||
|
|
|
@ -39,6 +39,7 @@ type controller struct {
|
||||||
clock pub.Clock
|
clock pub.Clock
|
||||||
client pub.HttpClient
|
client pub.HttpClient
|
||||||
appAgent string
|
appAgent string
|
||||||
|
log *logrus.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewController returns an implementation of the Controller interface for creating new transports
|
// NewController returns an implementation of the Controller interface for creating new transports
|
||||||
|
@ -48,6 +49,7 @@ func NewController(config *config.Config, clock pub.Clock, client pub.HttpClient
|
||||||
clock: clock,
|
clock: clock,
|
||||||
client: client,
|
client: client,
|
||||||
appAgent: fmt.Sprintf("%s %s", config.ApplicationName, config.Host),
|
appAgent: fmt.Sprintf("%s %s", config.ApplicationName, config.Host),
|
||||||
|
log: log,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,5 +82,6 @@ func (c *controller) NewTransport(pubKeyID string, privkey crypto.PrivateKey) (T
|
||||||
sigTransport: sigTransport,
|
sigTransport: sigTransport,
|
||||||
getSigner: getSigner,
|
getSigner: getSigner,
|
||||||
getSignerMu: &sync.Mutex{},
|
getSignerMu: &sync.Mutex{},
|
||||||
|
log: c.log,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
|
|
||||||
"github.com/go-fed/activity/pub"
|
"github.com/go-fed/activity/pub"
|
||||||
"github.com/go-fed/httpsig"
|
"github.com/go-fed/httpsig"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Transport wraps the pub.Transport interface with some additional
|
// Transport wraps the pub.Transport interface with some additional
|
||||||
|
@ -31,6 +32,7 @@ type transport struct {
|
||||||
sigTransport *pub.HttpSigTransport
|
sigTransport *pub.HttpSigTransport
|
||||||
getSigner httpsig.Signer
|
getSigner httpsig.Signer
|
||||||
getSignerMu *sync.Mutex
|
getSignerMu *sync.Mutex
|
||||||
|
log *logrus.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *transport) BatchDeliver(c context.Context, b []byte, recipients []*url.URL) error {
|
func (t *transport) BatchDeliver(c context.Context, b []byte, recipients []*url.URL) error {
|
||||||
|
@ -38,14 +40,20 @@ func (t *transport) BatchDeliver(c context.Context, b []byte, recipients []*url.
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *transport) Deliver(c context.Context, b []byte, to *url.URL) error {
|
func (t *transport) Deliver(c context.Context, b []byte, to *url.URL) error {
|
||||||
|
l := t.log.WithField("func", "Deliver")
|
||||||
|
l.Debugf("performing POST to %s", to.String())
|
||||||
return t.sigTransport.Deliver(c, b, to)
|
return t.sigTransport.Deliver(c, b, to)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *transport) Dereference(c context.Context, iri *url.URL) ([]byte, error) {
|
func (t *transport) Dereference(c context.Context, iri *url.URL) ([]byte, error) {
|
||||||
|
l := t.log.WithField("func", "Dereference")
|
||||||
|
l.Debugf("performing GET to %s", iri.String())
|
||||||
return t.sigTransport.Dereference(c, iri)
|
return t.sigTransport.Dereference(c, iri)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *transport) DereferenceMedia(c context.Context, iri *url.URL, expectedContentType string) ([]byte, error) {
|
func (t *transport) DereferenceMedia(c context.Context, iri *url.URL, expectedContentType string) ([]byte, error) {
|
||||||
|
l := t.log.WithField("func", "DereferenceMedia")
|
||||||
|
l.Debugf("performing GET to %s", iri.String())
|
||||||
req, err := http.NewRequest("GET", iri.String(), nil)
|
req, err := http.NewRequest("GET", iri.String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -37,7 +37,7 @@ func (c *converter) ASRepresentationToAccount(accountable Accountable) (*gtsmode
|
||||||
uri := uriProp.GetIRI()
|
uri := uriProp.GetIRI()
|
||||||
|
|
||||||
acct := >smodel.Account{}
|
acct := >smodel.Account{}
|
||||||
err := c.db.GetWhere("uri", uri.String(), acct)
|
err := c.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, acct)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// we already know this account so we can skip generating it
|
// we already know this account so we can skip generating it
|
||||||
return acct, nil
|
return acct, nil
|
||||||
|
@ -90,7 +90,7 @@ func (c *converter) ASRepresentationToAccount(accountable Accountable) (*gtsmode
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for bot and actor type
|
// check for bot and actor type
|
||||||
switch gtsmodel.ActivityStreamsActor(accountable.GetTypeName()) {
|
switch accountable.GetTypeName() {
|
||||||
case gtsmodel.ActivityStreamsPerson, gtsmodel.ActivityStreamsGroup, gtsmodel.ActivityStreamsOrganization:
|
case gtsmodel.ActivityStreamsPerson, gtsmodel.ActivityStreamsGroup, gtsmodel.ActivityStreamsOrganization:
|
||||||
// people, groups, and organizations aren't bots
|
// people, groups, and organizations aren't bots
|
||||||
acct.Bot = false
|
acct.Bot = false
|
||||||
|
@ -101,7 +101,7 @@ func (c *converter) ASRepresentationToAccount(accountable Accountable) (*gtsmode
|
||||||
// we don't know what this is!
|
// we don't know what this is!
|
||||||
return nil, fmt.Errorf("type name %s not recognised or not convertible to gtsmodel.ActivityStreamsActor", accountable.GetTypeName())
|
return nil, fmt.Errorf("type name %s not recognised or not convertible to gtsmodel.ActivityStreamsActor", accountable.GetTypeName())
|
||||||
}
|
}
|
||||||
acct.ActorType = gtsmodel.ActivityStreamsActor(accountable.GetTypeName())
|
acct.ActorType = accountable.GetTypeName()
|
||||||
|
|
||||||
// TODO: locked aka manuallyApprovesFollowers
|
// TODO: locked aka manuallyApprovesFollowers
|
||||||
|
|
||||||
|
@ -220,7 +220,7 @@ func (c *converter) ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, e
|
||||||
status.APStatusOwnerURI = attributedTo.String()
|
status.APStatusOwnerURI = attributedTo.String()
|
||||||
|
|
||||||
statusOwner := >smodel.Account{}
|
statusOwner := >smodel.Account{}
|
||||||
if err := c.db.GetWhere("uri", attributedTo.String(), statusOwner); err != nil {
|
if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: attributedTo.String()}}, statusOwner); err != nil {
|
||||||
return nil, fmt.Errorf("couldn't get status owner from db: %s", err)
|
return nil, fmt.Errorf("couldn't get status owner from db: %s", err)
|
||||||
}
|
}
|
||||||
status.AccountID = statusOwner.ID
|
status.AccountID = statusOwner.ID
|
||||||
|
@ -235,7 +235,7 @@ func (c *converter) ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, e
|
||||||
|
|
||||||
// now we can check if we have the replied-to status in our db already
|
// now we can check if we have the replied-to status in our db already
|
||||||
inReplyToStatus := >smodel.Status{}
|
inReplyToStatus := >smodel.Status{}
|
||||||
if err := c.db.GetWhere("uri", inReplyToURI.String(), inReplyToStatus); err == nil {
|
if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: inReplyToURI.String()}}, inReplyToStatus); err == nil {
|
||||||
// we have the status in our database already
|
// we have the status in our database already
|
||||||
// so we can set these fields here and then...
|
// so we can set these fields here and then...
|
||||||
status.InReplyToID = inReplyToStatus.ID
|
status.InReplyToID = inReplyToStatus.ID
|
||||||
|
@ -281,7 +281,10 @@ func (c *converter) ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, e
|
||||||
|
|
||||||
// if it's CC'ed to public, it's public or unlocked
|
// if it's CC'ed to public, it's public or unlocked
|
||||||
// mentioned SPECIFIC ACCOUNTS also get added to CC'es if it's not a direct message
|
// mentioned SPECIFIC ACCOUNTS also get added to CC'es if it's not a direct message
|
||||||
if isPublic(cc) || isPublic(to) {
|
if isPublic(cc) {
|
||||||
|
visibility = gtsmodel.VisibilityUnlocked
|
||||||
|
}
|
||||||
|
if isPublic(to) {
|
||||||
visibility = gtsmodel.VisibilityPublic
|
visibility = gtsmodel.VisibilityPublic
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,7 +304,7 @@ func (c *converter) ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, e
|
||||||
// we might be able to extract this from the contentMap field
|
// we might be able to extract this from the contentMap field
|
||||||
|
|
||||||
// ActivityStreamsType
|
// ActivityStreamsType
|
||||||
status.ActivityStreamsType = gtsmodel.ActivityStreamsObject(statusable.GetTypeName())
|
status.ActivityStreamsType = statusable.GetTypeName()
|
||||||
|
|
||||||
return status, nil
|
return status, nil
|
||||||
}
|
}
|
||||||
|
@ -319,7 +322,7 @@ func (c *converter) ASFollowToFollowRequest(followable Followable) (*gtsmodel.Fo
|
||||||
return nil, errors.New("error extracting actor property from follow")
|
return nil, errors.New("error extracting actor property from follow")
|
||||||
}
|
}
|
||||||
originAccount := >smodel.Account{}
|
originAccount := >smodel.Account{}
|
||||||
if err := c.db.GetWhere("uri", origin.String(), originAccount); err != nil {
|
if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: origin.String()}}, originAccount); err != nil {
|
||||||
return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err)
|
return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -328,7 +331,7 @@ func (c *converter) ASFollowToFollowRequest(followable Followable) (*gtsmodel.Fo
|
||||||
return nil, errors.New("error extracting object property from follow")
|
return nil, errors.New("error extracting object property from follow")
|
||||||
}
|
}
|
||||||
targetAccount := >smodel.Account{}
|
targetAccount := >smodel.Account{}
|
||||||
if err := c.db.GetWhere("uri", target.String(), targetAccount); err != nil {
|
if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: target.String()}}, targetAccount); err != nil {
|
||||||
return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err)
|
return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -341,6 +344,40 @@ func (c *converter) ASFollowToFollowRequest(followable Followable) (*gtsmodel.Fo
|
||||||
return followRequest, nil
|
return followRequest, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *converter) ASFollowToFollow(followable Followable) (*gtsmodel.Follow, error) {
|
||||||
|
idProp := followable.GetJSONLDId()
|
||||||
|
if idProp == nil || !idProp.IsIRI() {
|
||||||
|
return nil, errors.New("no id property set on follow, or was not an iri")
|
||||||
|
}
|
||||||
|
uri := idProp.GetIRI().String()
|
||||||
|
|
||||||
|
origin, err := extractActor(followable)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("error extracting actor property from follow")
|
||||||
|
}
|
||||||
|
originAccount := >smodel.Account{}
|
||||||
|
if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: origin.String()}}, originAccount); err != nil {
|
||||||
|
return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
target, err := extractObject(followable)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("error extracting object property from follow")
|
||||||
|
}
|
||||||
|
targetAccount := >smodel.Account{}
|
||||||
|
if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: target.String()}}, targetAccount); err != nil {
|
||||||
|
return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
follow := >smodel.Follow{
|
||||||
|
URI: uri,
|
||||||
|
AccountID: originAccount.ID,
|
||||||
|
TargetAccountID: targetAccount.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
return follow, nil
|
||||||
|
}
|
||||||
|
|
||||||
func isPublic(tos []*url.URL) bool {
|
func isPublic(tos []*url.URL) bool {
|
||||||
for _, entry := range tos {
|
for _, entry := range tos {
|
||||||
if strings.EqualFold(entry.String(), "https://www.w3.org/ns/activitystreams#Public") {
|
if strings.EqualFold(entry.String(), "https://www.w3.org/ns/activitystreams#Public") {
|
||||||
|
|
|
@ -26,6 +26,10 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
asPublicURI = "https://www.w3.org/ns/activitystreams#Public"
|
||||||
|
)
|
||||||
|
|
||||||
// TypeConverter is an interface for the common action of converting between apimodule (frontend, serializable) models,
|
// TypeConverter is an interface for the common action of converting between apimodule (frontend, serializable) models,
|
||||||
// internal gts models used in the database, and activitypub models used in federation.
|
// internal gts models used in the database, and activitypub models used in federation.
|
||||||
//
|
//
|
||||||
|
@ -77,6 +81,9 @@ type TypeConverter interface {
|
||||||
// InstanceToMasto converts a gts instance into its mastodon equivalent for serving at /api/v1/instance
|
// InstanceToMasto converts a gts instance into its mastodon equivalent for serving at /api/v1/instance
|
||||||
InstanceToMasto(i *gtsmodel.Instance) (*model.Instance, error)
|
InstanceToMasto(i *gtsmodel.Instance) (*model.Instance, error)
|
||||||
|
|
||||||
|
// RelationshipToMasto converts a gts relationship into its mastodon equivalent for serving in various places
|
||||||
|
RelationshipToMasto(r *gtsmodel.Relationship) (*model.Relationship, error)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
FRONTEND (mastodon) MODEL TO INTERNAL (gts) MODEL
|
FRONTEND (mastodon) MODEL TO INTERNAL (gts) MODEL
|
||||||
*/
|
*/
|
||||||
|
@ -94,6 +101,8 @@ type TypeConverter interface {
|
||||||
ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, error)
|
ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, error)
|
||||||
// ASFollowToFollowRequest converts a remote activitystreams `follow` representation into gts model follow request.
|
// ASFollowToFollowRequest converts a remote activitystreams `follow` representation into gts model follow request.
|
||||||
ASFollowToFollowRequest(followable Followable) (*gtsmodel.FollowRequest, error)
|
ASFollowToFollowRequest(followable Followable) (*gtsmodel.FollowRequest, error)
|
||||||
|
// ASFollowToFollowRequest converts a remote activitystreams `follow` representation into gts model follow.
|
||||||
|
ASFollowToFollow(followable Followable) (*gtsmodel.Follow, error)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
INTERNAL (gts) MODEL TO ACTIVITYSTREAMS MODEL
|
INTERNAL (gts) MODEL TO ACTIVITYSTREAMS MODEL
|
||||||
|
@ -104,6 +113,15 @@ type TypeConverter interface {
|
||||||
|
|
||||||
// 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)
|
||||||
|
|
||||||
|
// FollowToASFollow converts a gts model Follow into an activity streams Follow, suitable for federation
|
||||||
|
FollowToAS(f *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (vocab.ActivityStreamsFollow, error)
|
||||||
|
|
||||||
|
// MentionToAS converts a gts model mention into an activity streams Mention, suitable for federation
|
||||||
|
MentionToAS(m *gtsmodel.Mention) (vocab.ActivityStreamsMention, error)
|
||||||
|
|
||||||
|
// AttachmentToAS converts a gts model media attachment into an activity streams Attachment, suitable for federation
|
||||||
|
AttachmentToAS(a *gtsmodel.MediaAttachment) (vocab.ActivityStreamsDocument, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type converter struct {
|
type converter struct {
|
||||||
|
|
|
@ -21,10 +21,12 @@ package typeutils
|
||||||
import (
|
import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/go-fed/activity/streams"
|
"github.com/go-fed/activity/streams"
|
||||||
"github.com/go-fed/activity/streams/vocab"
|
"github.com/go-fed/activity/streams/vocab"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -256,5 +258,304 @@ func (c *converter) AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerso
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, error) {
|
func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, error) {
|
||||||
return nil, nil
|
// ensure prerequisites here before we get stuck in
|
||||||
|
|
||||||
|
// check if author account is already attached to status and attach it if not
|
||||||
|
// if we can't retrieve this, bail here already because we can't attribute the status to anyone
|
||||||
|
if s.GTSAccount == nil {
|
||||||
|
a := >smodel.Account{}
|
||||||
|
if err := c.db.GetByID(s.AccountID, a); err != nil {
|
||||||
|
return nil, fmt.Errorf("StatusToAS: error retrieving author account from db: %s", err)
|
||||||
|
}
|
||||||
|
s.GTSAccount = a
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the Note!
|
||||||
|
status := streams.NewActivityStreamsNote()
|
||||||
|
|
||||||
|
// id
|
||||||
|
statusURI, err := url.Parse(s.URI)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.URI, err)
|
||||||
|
}
|
||||||
|
statusIDProp := streams.NewJSONLDIdProperty()
|
||||||
|
statusIDProp.SetIRI(statusURI)
|
||||||
|
status.SetJSONLDId(statusIDProp)
|
||||||
|
|
||||||
|
// type
|
||||||
|
// will be set automatically by go-fed
|
||||||
|
|
||||||
|
// summary aka cw
|
||||||
|
statusSummaryProp := streams.NewActivityStreamsSummaryProperty()
|
||||||
|
statusSummaryProp.AppendXMLSchemaString(s.ContentWarning)
|
||||||
|
status.SetActivityStreamsSummary(statusSummaryProp)
|
||||||
|
|
||||||
|
// inReplyTo
|
||||||
|
if s.InReplyToID != "" {
|
||||||
|
// fetch the replied status if we don't have it on hand already
|
||||||
|
if s.GTSReplyToStatus == nil {
|
||||||
|
rs := >smodel.Status{}
|
||||||
|
if err := c.db.GetByID(s.InReplyToID, rs); err != nil {
|
||||||
|
return nil, fmt.Errorf("StatusToAS: error retrieving replied-to status from db: %s", err)
|
||||||
|
}
|
||||||
|
s.GTSReplyToStatus = rs
|
||||||
|
}
|
||||||
|
rURI, err := url.Parse(s.GTSReplyToStatus.URI)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.GTSReplyToStatus.URI, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
inReplyToProp := streams.NewActivityStreamsInReplyToProperty()
|
||||||
|
inReplyToProp.AppendIRI(rURI)
|
||||||
|
status.SetActivityStreamsInReplyTo(inReplyToProp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// published
|
||||||
|
publishedProp := streams.NewActivityStreamsPublishedProperty()
|
||||||
|
publishedProp.Set(s.CreatedAt)
|
||||||
|
status.SetActivityStreamsPublished(publishedProp)
|
||||||
|
|
||||||
|
// url
|
||||||
|
if s.URL != "" {
|
||||||
|
sURL, err := url.Parse(s.URL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.URL, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
urlProp := streams.NewActivityStreamsUrlProperty()
|
||||||
|
urlProp.AppendIRI(sURL)
|
||||||
|
status.SetActivityStreamsUrl(urlProp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// attributedTo
|
||||||
|
authorAccountURI, err := url.Parse(s.GTSAccount.URI)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.GTSAccount.URI, err)
|
||||||
|
}
|
||||||
|
attributedToProp := streams.NewActivityStreamsAttributedToProperty()
|
||||||
|
attributedToProp.AppendIRI(authorAccountURI)
|
||||||
|
status.SetActivityStreamsAttributedTo(attributedToProp)
|
||||||
|
|
||||||
|
// tags
|
||||||
|
tagProp := streams.NewActivityStreamsTagProperty()
|
||||||
|
|
||||||
|
// tag -- mentions
|
||||||
|
for _, m := range s.GTSMentions {
|
||||||
|
asMention, err := c.MentionToAS(m)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("StatusToAS: error converting mention to AS mention: %s", err)
|
||||||
|
}
|
||||||
|
tagProp.AppendActivityStreamsMention(asMention)
|
||||||
|
}
|
||||||
|
|
||||||
|
// tag -- emojis
|
||||||
|
// TODO
|
||||||
|
|
||||||
|
// tag -- hashtags
|
||||||
|
// TODO
|
||||||
|
|
||||||
|
status.SetActivityStreamsTag(tagProp)
|
||||||
|
|
||||||
|
// parse out some URIs we need here
|
||||||
|
authorFollowersURI, err := url.Parse(s.GTSAccount.FollowersURI)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.GTSAccount.FollowersURI, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
publicURI, err := url.Parse(asPublicURI)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", asPublicURI, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// to and cc
|
||||||
|
toProp := streams.NewActivityStreamsToProperty()
|
||||||
|
ccProp := streams.NewActivityStreamsCcProperty()
|
||||||
|
switch s.Visibility {
|
||||||
|
case gtsmodel.VisibilityDirect:
|
||||||
|
// if DIRECT, then only mentioned users should be added to TO, and nothing to CC
|
||||||
|
for _, m := range s.GTSMentions {
|
||||||
|
iri, err := url.Parse(m.GTSAccount.URI)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.GTSAccount.URI, err)
|
||||||
|
}
|
||||||
|
toProp.AppendIRI(iri)
|
||||||
|
}
|
||||||
|
case gtsmodel.VisibilityMutualsOnly:
|
||||||
|
// TODO
|
||||||
|
case gtsmodel.VisibilityFollowersOnly:
|
||||||
|
// if FOLLOWERS ONLY then we want to add followers to TO, and mentions to CC
|
||||||
|
toProp.AppendIRI(authorFollowersURI)
|
||||||
|
for _, m := range s.GTSMentions {
|
||||||
|
iri, err := url.Parse(m.GTSAccount.URI)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.GTSAccount.URI, err)
|
||||||
|
}
|
||||||
|
ccProp.AppendIRI(iri)
|
||||||
|
}
|
||||||
|
case gtsmodel.VisibilityUnlocked:
|
||||||
|
// if UNLOCKED, we want to add followers to TO, and public and mentions to CC
|
||||||
|
toProp.AppendIRI(authorFollowersURI)
|
||||||
|
ccProp.AppendIRI(publicURI)
|
||||||
|
for _, m := range s.GTSMentions {
|
||||||
|
iri, err := url.Parse(m.GTSAccount.URI)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.GTSAccount.URI, err)
|
||||||
|
}
|
||||||
|
ccProp.AppendIRI(iri)
|
||||||
|
}
|
||||||
|
case gtsmodel.VisibilityPublic:
|
||||||
|
// if PUBLIC, we want to add public to TO, and followers and mentions to CC
|
||||||
|
toProp.AppendIRI(publicURI)
|
||||||
|
ccProp.AppendIRI(authorFollowersURI)
|
||||||
|
for _, m := range s.GTSMentions {
|
||||||
|
iri, err := url.Parse(m.GTSAccount.URI)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.GTSAccount.URI, err)
|
||||||
|
}
|
||||||
|
ccProp.AppendIRI(iri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status.SetActivityStreamsTo(toProp)
|
||||||
|
status.SetActivityStreamsCc(ccProp)
|
||||||
|
|
||||||
|
// conversation
|
||||||
|
// TODO
|
||||||
|
|
||||||
|
// content -- the actual post itself
|
||||||
|
contentProp := streams.NewActivityStreamsContentProperty()
|
||||||
|
contentProp.AppendXMLSchemaString(s.Content)
|
||||||
|
status.SetActivityStreamsContent(contentProp)
|
||||||
|
|
||||||
|
// attachment
|
||||||
|
attachmentProp := streams.NewActivityStreamsAttachmentProperty()
|
||||||
|
for _, a := range s.GTSMediaAttachments {
|
||||||
|
doc, err := c.AttachmentToAS(a)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("StatusToAS: error converting attachment: %s", err)
|
||||||
|
}
|
||||||
|
attachmentProp.AppendActivityStreamsDocument(doc)
|
||||||
|
}
|
||||||
|
status.SetActivityStreamsAttachment(attachmentProp)
|
||||||
|
|
||||||
|
// replies
|
||||||
|
// TODO
|
||||||
|
|
||||||
|
return status, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *converter) FollowToAS(f *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (vocab.ActivityStreamsFollow, error) {
|
||||||
|
// parse out the various URIs we need for this
|
||||||
|
// origin account (who's doing the follow)
|
||||||
|
originAccountURI, err := url.Parse(originAccount.URI)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("followtoasfollow: error parsing origin account uri: %s", err)
|
||||||
|
}
|
||||||
|
originActor := streams.NewActivityStreamsActorProperty()
|
||||||
|
originActor.AppendIRI(originAccountURI)
|
||||||
|
|
||||||
|
// target account (who's being followed)
|
||||||
|
targetAccountURI, err := url.Parse(targetAccount.URI)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("followtoasfollow: error parsing target account uri: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// uri of the follow activity itself
|
||||||
|
followURI, err := url.Parse(f.URI)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("followtoasfollow: error parsing follow uri: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// start preparing the follow activity
|
||||||
|
follow := streams.NewActivityStreamsFollow()
|
||||||
|
|
||||||
|
// set the actor
|
||||||
|
follow.SetActivityStreamsActor(originActor)
|
||||||
|
|
||||||
|
// set the id
|
||||||
|
followIDProp := streams.NewJSONLDIdProperty()
|
||||||
|
followIDProp.SetIRI(followURI)
|
||||||
|
follow.SetJSONLDId(followIDProp)
|
||||||
|
|
||||||
|
// set the object
|
||||||
|
followObjectProp := streams.NewActivityStreamsObjectProperty()
|
||||||
|
followObjectProp.AppendIRI(targetAccountURI)
|
||||||
|
follow.SetActivityStreamsObject(followObjectProp)
|
||||||
|
|
||||||
|
// set the To property
|
||||||
|
followToProp := streams.NewActivityStreamsToProperty()
|
||||||
|
followToProp.AppendIRI(targetAccountURI)
|
||||||
|
follow.SetActivityStreamsTo(followToProp)
|
||||||
|
|
||||||
|
return follow, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *converter) MentionToAS(m *gtsmodel.Mention) (vocab.ActivityStreamsMention, error) {
|
||||||
|
if m.GTSAccount == nil {
|
||||||
|
a := >smodel.Account{}
|
||||||
|
if err := c.db.GetWhere([]db.Where{{Key: "target_account_id", Value: m.TargetAccountID}}, a); err != nil {
|
||||||
|
return nil, fmt.Errorf("MentionToAS: error getting target account from db: %s", err)
|
||||||
|
}
|
||||||
|
m.GTSAccount = a
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the mention
|
||||||
|
mention := streams.NewActivityStreamsMention()
|
||||||
|
|
||||||
|
// href -- this should be the URI of the mentioned user
|
||||||
|
hrefProp := streams.NewActivityStreamsHrefProperty()
|
||||||
|
hrefURI, err := url.Parse(m.GTSAccount.URI)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("MentionToAS: error parsing uri %s: %s", m.GTSAccount.URI, err)
|
||||||
|
}
|
||||||
|
hrefProp.SetIRI(hrefURI)
|
||||||
|
mention.SetActivityStreamsHref(hrefProp)
|
||||||
|
|
||||||
|
// name -- this should be the namestring of the mentioned user, something like @whatever@example.org
|
||||||
|
var domain string
|
||||||
|
if m.GTSAccount.Domain == "" {
|
||||||
|
domain = c.config.Host
|
||||||
|
} else {
|
||||||
|
domain = m.GTSAccount.Domain
|
||||||
|
}
|
||||||
|
username := m.GTSAccount.Username
|
||||||
|
nameString := fmt.Sprintf("@%s@%s", username, domain)
|
||||||
|
nameProp := streams.NewActivityStreamsNameProperty()
|
||||||
|
nameProp.AppendXMLSchemaString(nameString)
|
||||||
|
mention.SetActivityStreamsName(nameProp)
|
||||||
|
|
||||||
|
return mention, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *converter) AttachmentToAS(a *gtsmodel.MediaAttachment) (vocab.ActivityStreamsDocument, error) {
|
||||||
|
// type -- Document
|
||||||
|
doc := streams.NewActivityStreamsDocument()
|
||||||
|
|
||||||
|
// mediaType aka mime content type
|
||||||
|
mediaTypeProp := streams.NewActivityStreamsMediaTypeProperty()
|
||||||
|
mediaTypeProp.Set(a.File.ContentType)
|
||||||
|
doc.SetActivityStreamsMediaType(mediaTypeProp)
|
||||||
|
|
||||||
|
// url -- for the original image not the thumbnail
|
||||||
|
urlProp := streams.NewActivityStreamsUrlProperty()
|
||||||
|
imageURL, err := url.Parse(a.URL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("AttachmentToAS: error parsing uri %s: %s", a.URL, err)
|
||||||
|
}
|
||||||
|
urlProp.AppendIRI(imageURL)
|
||||||
|
doc.SetActivityStreamsUrl(urlProp)
|
||||||
|
|
||||||
|
// name -- aka image description
|
||||||
|
nameProp := streams.NewActivityStreamsNameProperty()
|
||||||
|
nameProp.AppendXMLSchemaString(a.Description)
|
||||||
|
doc.SetActivityStreamsName(nameProp)
|
||||||
|
|
||||||
|
// blurhash
|
||||||
|
blurProp := streams.NewTootBlurhashProperty()
|
||||||
|
blurProp.Set(a.Blurhash)
|
||||||
|
doc.SetTootBlurhash(blurProp)
|
||||||
|
|
||||||
|
// focalpoint
|
||||||
|
// TODO
|
||||||
|
|
||||||
|
return doc, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -572,3 +572,21 @@ func (c *converter) InstanceToMasto(i *gtsmodel.Instance) (*model.Instance, erro
|
||||||
|
|
||||||
return mi, nil
|
return mi, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *converter) RelationshipToMasto(r *gtsmodel.Relationship) (*model.Relationship, error) {
|
||||||
|
return &model.Relationship{
|
||||||
|
ID: r.ID,
|
||||||
|
Following: r.Following,
|
||||||
|
ShowingReblogs: r.ShowingReblogs,
|
||||||
|
Notifying: r.Notifying,
|
||||||
|
FollowedBy: r.FollowedBy,
|
||||||
|
Blocking: r.Blocking,
|
||||||
|
BlockedBy: r.BlockedBy,
|
||||||
|
Muting: r.Muting,
|
||||||
|
MutingNotifications: r.MutingNotifications,
|
||||||
|
Requested: r.Requested,
|
||||||
|
DomainBlocking: r.DomainBlocking,
|
||||||
|
Endorsed: r.Endorsed,
|
||||||
|
Note: r.Note,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
|
@ -22,6 +22,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -47,6 +49,8 @@ const (
|
||||||
FeaturedPath = "featured"
|
FeaturedPath = "featured"
|
||||||
// PublicKeyPath is for serving an account's public key
|
// PublicKeyPath is for serving an account's public key
|
||||||
PublicKeyPath = "main-key"
|
PublicKeyPath = "main-key"
|
||||||
|
// FollowPath used to generate the URI for an individual follow or follow request
|
||||||
|
FollowPath = "follow"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -103,6 +107,12 @@ type UserURIs struct {
|
||||||
PublicKeyURI string
|
PublicKeyURI string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateURIForFollow returns the AP URI for a new follow -- something like:
|
||||||
|
// https://example.org/users/whatever_user/follow/41c7f33f-1060-48d9-84df-38dcb13cf0d8
|
||||||
|
func GenerateURIForFollow(username string, protocol string, host string) string {
|
||||||
|
return fmt.Sprintf("%s://%s/%s/%s/%s", protocol, host, UsersPath, FollowPath, uuid.NewString())
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|
|
@ -48,6 +48,7 @@ import (
|
||||||
var Run action.GTSAction = func(ctx context.Context, _ *config.Config, log *logrus.Logger) error {
|
var Run action.GTSAction = func(ctx context.Context, _ *config.Config, log *logrus.Logger) error {
|
||||||
c := NewTestConfig()
|
c := NewTestConfig()
|
||||||
dbService := NewTestDB()
|
dbService := NewTestDB()
|
||||||
|
federatingDB := NewTestFederatingDB(dbService)
|
||||||
router := NewTestRouter()
|
router := NewTestRouter()
|
||||||
storageBackend := NewTestStorage()
|
storageBackend := NewTestStorage()
|
||||||
|
|
||||||
|
@ -59,7 +60,7 @@ var Run action.GTSAction = func(ctx context.Context, _ *config.Config, log *logr
|
||||||
Body: r,
|
Body: r,
|
||||||
}, nil
|
}, nil
|
||||||
}))
|
}))
|
||||||
federator := federation.NewFederator(dbService, transportController, c, log, typeConverter)
|
federator := federation.NewFederator(dbService, federatingDB, transportController, c, log, typeConverter)
|
||||||
processor := NewTestProcessor(dbService, storageBackend, federator)
|
processor := NewTestProcessor(dbService, storageBackend, federator)
|
||||||
if err := processor.Start(); err != nil {
|
if err := processor.Start(); err != nil {
|
||||||
return fmt.Errorf("error starting processor: %s", err)
|
return fmt.Errorf("error starting processor: %s", err)
|
||||||
|
|
11
testrig/federatingdb.go
Normal file
11
testrig/federatingdb.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package testrig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewTestFederatingDB returns a federating DB with the underlying db
|
||||||
|
func NewTestFederatingDB(db db.DB) federation.FederatingDB {
|
||||||
|
return federation.NewFederatingDB(db, NewTestConfig(), NewTestLog())
|
||||||
|
}
|
|
@ -26,5 +26,5 @@ import (
|
||||||
|
|
||||||
// NewTestFederator returns a federator with the given database and (mock!!) transport controller.
|
// NewTestFederator returns a federator with the given database and (mock!!) transport controller.
|
||||||
func NewTestFederator(db db.DB, tc transport.Controller) federation.Federator {
|
func NewTestFederator(db db.DB, tc transport.Controller) federation.Federator {
|
||||||
return federation.NewFederator(db, tc, NewTestConfig(), NewTestLog(), NewTestTypeConverter(db))
|
return federation.NewFederator(db, NewTestFederatingDB(db), tc, NewTestConfig(), NewTestLog(), NewTestTypeConverter(db))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue