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:
Tobi Smethurst 2021-05-21 15:48:26 +02:00 committed by GitHub
parent dc06e71b76
commit d839f27c30
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 2260 additions and 299 deletions

View file

@ -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

View file

@ -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
} }

View 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)
}

View file

@ -0,0 +1,49 @@
/*
GoToSocial
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package account
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
// 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)
}

View 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)
}

View 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)
}

View file

@ -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 := &gtsmodel.Application{ app := &gtsmodel.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
} }

View file

@ -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 := &gtsmodel.Application{} app := &gtsmodel.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)

View file

@ -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 := &gtsmodel.User{} gtsUser := &gtsmodel.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()
} }

View file

@ -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)
} }

View file

@ -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 := &gtsmodel.Tag{} gtsTag := &gtsmodel.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)
} }

View file

@ -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"`
} }

View 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)
}

View 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)
}

View file

@ -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
} }

View file

@ -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
} }

View 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
}
}

View file

@ -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)

View file

@ -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 := &gtsmodel.FollowRequest{} fr := &gtsmodel.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 := &gtsmodel.Follow{ follow := &gtsmodel.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(&gtsmodel.FollowRequest{}).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Delete(); err != nil { if _, err := ps.conn.Model(&gtsmodel.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 := &gtsmodel.Relationship{
ID: targetAccount,
}
// check if the requesting account follows the target account
follow := &gtsmodel.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(&gtsmodel.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(&gtsmodel.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(&gtsmodel.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(&gtsmodel.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(&gtsmodel.Follow{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists() return ps.conn.Model(&gtsmodel.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(&gtsmodel.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(&gtsmodel.Follow{}).Where("account_id = ?", account1.ID).Where("target_account_id = ?", account2.ID).Exists() f1, err := ps.conn.Model(&gtsmodel.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 := &gtsmodel.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, &gtsmodel.Mention{ menchies = append(menchies, &gtsmodel.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

View file

@ -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, &gtsmodel.Status{}); err != nil { if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uid}}, &gtsmodel.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 := &gtsmodel.Account{} acct := &gtsmodel.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 := &gtsmodel.Account{} acct := &gtsmodel.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 := &gtsmodel.Account{} acct := &gtsmodel.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 := &gtsmodel.Account{} acct := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.Account{} acct := &gtsmodel.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 := &gtsmodel.Account{} acct := &gtsmodel.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}}, &gtsmodel.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}}, &gtsmodel.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
}

View file

@ -124,7 +124,7 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr
} }
requestingAccount := &gtsmodel.Account{} requestingAccount := &gtsmodel.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 := &gtsmodel.Account{} a := &gtsmodel.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
} }

View file

@ -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
}

View file

@ -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()

View file

@ -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 := &gtsmodel.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
}

View file

@ -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)

View file

@ -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

View file

@ -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"
) )

View file

@ -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:"-"`
} }

View file

@ -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
} }

View file

@ -0,0 +1,49 @@
/*
GoToSocial
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package 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
}

View file

@ -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?

View file

@ -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)
}

View file

@ -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)

View file

@ -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,

View file

@ -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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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: &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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: &gtsmodel.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: &gtsmodel.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)
}

View file

@ -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 := &gtsmodel.Account{} requestingAccount := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.Account{} requestedAccount := &gtsmodel.Account{}

View file

@ -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 := &gtsmodel.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
} }

View file

@ -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
}

View file

@ -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 := &gtsmodel.MediaAttachment{} maybeAttachment := &gtsmodel.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 := &gtsmodel.Account{} targetAccount := &gtsmodel.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
}

View file

@ -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 {

View file

@ -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 := &gtsmodel.Instance{} i := &gtsmodel.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))
} }

View file

@ -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 {

View file

@ -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, &gtsmodel.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, &gtsmodel.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
}

View file

@ -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

View file

@ -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
} }

View file

@ -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

View file

@ -37,7 +37,7 @@ func (c *converter) ASRepresentationToAccount(accountable Accountable) (*gtsmode
uri := uriProp.GetIRI() uri := uriProp.GetIRI()
acct := &gtsmodel.Account{} acct := &gtsmodel.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 := &gtsmodel.Account{} statusOwner := &gtsmodel.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 := &gtsmodel.Status{} inReplyToStatus := &gtsmodel.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 := &gtsmodel.Account{} originAccount := &gtsmodel.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 := &gtsmodel.Account{} targetAccount := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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") {

View file

@ -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 {

View file

@ -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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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
} }

View file

@ -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
}

View file

@ -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

View file

@ -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
View 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())
}

View file

@ -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))
} }