mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2024-11-23 04:43:13 +00:00
Follows and relationships (#27)
* Follows -- create and undo, both remote and local * Statuses -- federate new posts, including media, attachments, CWs and image descriptions.
This commit is contained in:
parent
dc06e71b76
commit
d839f27c30
54 changed files with 2260 additions and 299 deletions
|
@ -17,12 +17,12 @@
|
|||
* [x] /api/v1/accounts/:id GET (Get account information)
|
||||
* [x] /api/v1/accounts/:id/statuses GET (Get an account's statuses)
|
||||
* [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/lists GET (Get lists containing this account)
|
||||
* [ ] /api/v1/accounts/:id/identity_proofs GET (Get identity proofs for this account)
|
||||
* [ ] /api/v1/accounts/:id/follow POST (Follow this account)
|
||||
* [ ] /api/v1/accounts/:id/unfollow POST (Unfollow this account)
|
||||
* [x] /api/v1/accounts/:id/follow POST (Follow this account)
|
||||
* [x] /api/v1/accounts/:id/unfollow POST (Unfollow this account)
|
||||
* [ ] /api/v1/accounts/:id/block POST (Block this account)
|
||||
* [ ] /api/v1/accounts/:id/unblock POST (Unblock this account)
|
||||
* [ ] /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/unpin POST (Remove this account from profile)
|
||||
* [ ] /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)
|
||||
* [ ] Bookmarks
|
||||
* [ ] /api/v1/bookmarks GET (See bookmarked statuses)
|
||||
|
@ -177,6 +177,7 @@
|
|||
* [ ] 'Greedy' federation
|
||||
* [ ] No federation (insulate this instance from the Fediverse)
|
||||
* [ ] Allowlist
|
||||
* [x] Secure HTTP signatures (creation and validation)
|
||||
* [ ] Storage
|
||||
* [x] Internal/statuses/preferences etc
|
||||
* [x] Postgres interface
|
||||
|
|
|
@ -57,6 +57,14 @@ const (
|
|||
GetStatusesPath = BasePathWithID + "/statuses"
|
||||
// GetFollowersPath is for showing an account's 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
|
||||
|
@ -82,6 +90,10 @@ func (m *Module) Route(r router.Router) error {
|
|||
r.AttachHandler(http.MethodPatch, BasePathWithID, m.muxHandler)
|
||||
r.AttachHandler(http.MethodGet, GetStatusesPath, m.AccountStatusesGETHandler)
|
||||
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
|
||||
}
|
||||
|
||||
|
|
56
internal/api/client/account/follow.go
Normal file
56
internal/api/client/account/follow.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package account
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// AccountFollowPOSTHandler is the endpoint for creating a new follow request to the target account
|
||||
func (m *Module) AccountFollowPOSTHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
targetAcctID := c.Param(IDKey)
|
||||
if targetAcctID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
|
||||
return
|
||||
}
|
||||
form := &model.AccountFollowRequest{}
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
form.TargetAccountID = targetAcctID
|
||||
|
||||
relationship, errWithCode := m.processor.AccountFollowCreate(authed, form)
|
||||
if errWithCode != nil {
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, relationship)
|
||||
}
|
49
internal/api/client/account/following.go
Normal file
49
internal/api/client/account/following.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package account
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// AccountFollowersGETHandler serves the followers of the requested account, if they're visible to the requester.
|
||||
func (m *Module) AccountFollowingGETHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
targetAcctID := c.Param(IDKey)
|
||||
if targetAcctID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
|
||||
return
|
||||
}
|
||||
|
||||
following, errWithCode := m.processor.AccountFollowingGet(authed, targetAcctID)
|
||||
if errWithCode != nil {
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, following)
|
||||
}
|
46
internal/api/client/account/relationships.go
Normal file
46
internal/api/client/account/relationships.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
package account
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// AccountRelationshipsGETHandler serves the relationship of the requesting account with one or more requested account IDs.
|
||||
func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) {
|
||||
l := m.log.WithField("func", "AccountRelationshipsGETHandler")
|
||||
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debugf("error authing: %s", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
targetAccountIDs := c.QueryArray("id[]")
|
||||
if len(targetAccountIDs) == 0 {
|
||||
// check fallback -- let's be generous and see if maybe it's just set as 'id'?
|
||||
id := c.Query("id")
|
||||
if id == "" {
|
||||
l.Debug("no account id specified in query")
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
|
||||
return
|
||||
}
|
||||
targetAccountIDs = append(targetAccountIDs, id)
|
||||
}
|
||||
|
||||
relationships := []model.Relationship{}
|
||||
|
||||
for _, targetAccountID := range targetAccountIDs {
|
||||
r, errWithCode := m.processor.AccountRelationshipGet(authed, targetAccountID)
|
||||
if err != nil {
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
return
|
||||
}
|
||||
relationships = append(relationships, *r)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, relationships)
|
||||
}
|
53
internal/api/client/account/unfollow.go
Normal file
53
internal/api/client/account/unfollow.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package account
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// AccountUnfollowPOSTHandler is the endpoint for removing a follow and/or follow request to the target account
|
||||
func (m *Module) AccountUnfollowPOSTHandler(c *gin.Context) {
|
||||
l := m.log.WithField("func", "AccountUnfollowPOSTHandler")
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
l.Debug(err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
targetAcctID := c.Param(IDKey)
|
||||
if targetAcctID == "" {
|
||||
l.Debug(err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
|
||||
return
|
||||
}
|
||||
|
||||
relationship, errWithCode := m.processor.AccountFollowRemove(authed, targetAcctID)
|
||||
if errWithCode != nil {
|
||||
l.Debug(errWithCode.Error())
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, relationship)
|
||||
}
|
|
@ -28,6 +28,7 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
|
@ -60,7 +61,7 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) {
|
|||
app := >smodel.Application{
|
||||
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)})
|
||||
return
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ package auth
|
|||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
@ -68,7 +69,7 @@ func (m *Module) OauthTokenMiddleware(c *gin.Context) {
|
|||
if cid := ti.GetClientID(); cid != "" {
|
||||
l.Tracef("authenticated client %s with bearer token, scope is %s", cid, ti.GetScope())
|
||||
app := >smodel.Application{}
|
||||
if err := m.db.GetWhere("client_id", cid, app); err != nil {
|
||||
if err := m.db.GetWhere([]db.Where{{Key: "client_id",Value: cid}}, app); err != nil {
|
||||
l.Tracef("no app found for client %s", cid)
|
||||
}
|
||||
c.Set(oauth.SessionAuthorizedApplication, app)
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"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
|
||||
gtsUser := >smodel.User{}
|
||||
|
||||
if err := m.db.GetWhere("email", email, gtsUser); err != nil {
|
||||
if err := m.db.GetWhere([]db.Where{{Key: "email", Value: email}}, gtsUser); err != nil {
|
||||
l.Debugf("user %s was not retrievable from db during oauth authorization attempt: %s", email, err)
|
||||
return incorrectPassword()
|
||||
}
|
||||
|
|
|
@ -48,10 +48,11 @@ func (m *Module) FollowRequestAcceptPOSTHandler(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
if errWithCode := m.processor.FollowRequestAccept(authed, originAccountID); errWithCode != nil {
|
||||
r, errWithCode := m.processor.FollowRequestAccept(authed, originAccountID)
|
||||
if errWithCode != nil {
|
||||
l.Debug(errWithCode.Error())
|
||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusOK)
|
||||
c.JSON(http.StatusOK, r)
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ import (
|
|||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/status"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
|
@ -118,7 +119,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatus() {
|
|||
}, statusReply.Tags[0])
|
||||
|
||||
gtsTag := >smodel.Tag{}
|
||||
err = suite.db.GetWhere("name", "helloworld", gtsTag)
|
||||
err = suite.db.GetWhere([]db.Where{{Key: "name", Value: "helloworld"}}, gtsTag)
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.Equal(suite.T(), statusReply.Account.ID, gtsTag.FirstSeenFromAccountID)
|
||||
}
|
||||
|
|
|
@ -77,18 +77,18 @@ type Account struct {
|
|||
// See https://docs.joinmastodon.org/methods/accounts/
|
||||
type AccountCreateRequest struct {
|
||||
// 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
|
||||
Username string `form:"username" binding:"required"`
|
||||
Username string `form:"username" json:"username" xml:"username" binding:"required"`
|
||||
// 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
|
||||
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.
|
||||
// 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
|
||||
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
|
||||
IP net.IP `form:"-"`
|
||||
}
|
||||
|
@ -97,40 +97,51 @@ type AccountCreateRequest struct {
|
|||
// See https://docs.joinmastodon.org/methods/accounts/
|
||||
type UpdateCredentialsRequest struct {
|
||||
// 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.
|
||||
Bot *bool `form:"bot"`
|
||||
Bot *bool `form:"bot" json:"bot" xml:"bot"`
|
||||
// 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.
|
||||
Note *string `form:"note"`
|
||||
Note *string `form:"note" json:"note" xml:"note"`
|
||||
// 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 *multipart.FileHeader `form:"header"`
|
||||
Header *multipart.FileHeader `form:"header" json:"header" xml:"header"`
|
||||
// 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
|
||||
Source *UpdateSource `form:"source"`
|
||||
Source *UpdateSource `form:"source" json:"source" xml:"source"`
|
||||
// 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.
|
||||
type UpdateSource struct {
|
||||
// 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.
|
||||
Sensitive *bool `form:"sensitive"`
|
||||
Sensitive *bool `form:"sensitive" json:"sensitive" xml:"sensitive"`
|
||||
// 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.
|
||||
// By default, max 4 fields and 255 characters per property/value.
|
||||
type UpdateField struct {
|
||||
// Name of the field
|
||||
Name *string `form:"name"`
|
||||
Name *string `form:"name" json:"name" xml:"name"`
|
||||
// Value of the field
|
||||
Value *string `form:"value"`
|
||||
Value *string `form:"value" json:"value" xml:"value"`
|
||||
}
|
||||
|
||||
// AccountFollowRequest is for parsing requests at /api/v1/accounts/:id/follow
|
||||
type AccountFollowRequest struct {
|
||||
// ID of the account to follow request
|
||||
// This should be a URL parameter not a form field
|
||||
TargetAccountID string `form:"-"`
|
||||
// Show reblogs for this account?
|
||||
Reblogs *bool `form:"reblogs" json:"reblogs" xml:"reblogs"`
|
||||
// Notify when this account posts?
|
||||
Notify *bool `form:"notify" json:"notify" xml:"notify"`
|
||||
}
|
||||
|
|
58
internal/api/s2s/user/followers.go
Normal file
58
internal/api/s2s/user/followers.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (m *Module) FollowersGETHandler(c *gin.Context) {
|
||||
l := m.log.WithFields(logrus.Fields{
|
||||
"func": "FollowersGETHandler",
|
||||
"url": c.Request.RequestURI,
|
||||
})
|
||||
|
||||
requestedUsername := c.Param(UsernameKey)
|
||||
if requestedUsername == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"})
|
||||
return
|
||||
}
|
||||
|
||||
// make sure this actually an AP request
|
||||
format := c.NegotiateFormat(ActivityPubAcceptHeaders...)
|
||||
if format == "" {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": "could not negotiate format with given Accept header(s)"})
|
||||
return
|
||||
}
|
||||
l.Tracef("negotiated format: %s", format)
|
||||
|
||||
// make a copy of the context to pass along so we don't break anything
|
||||
cp := c.Copy()
|
||||
user, err := m.processor.GetFediFollowers(requestedUsername, cp.Request) // GetFediUser handles auth as well
|
||||
if err != nil {
|
||||
l.Info(err.Error())
|
||||
c.JSON(err.Code(), gin.H{"error": err.Safe()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
46
internal/api/s2s/user/statusget.go
Normal file
46
internal/api/s2s/user/statusget.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (m *Module) StatusGETHandler(c *gin.Context) {
|
||||
l := m.log.WithFields(logrus.Fields{
|
||||
"func": "StatusGETHandler",
|
||||
"url": c.Request.RequestURI,
|
||||
})
|
||||
|
||||
requestedUsername := c.Param(UsernameKey)
|
||||
if requestedUsername == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"})
|
||||
return
|
||||
}
|
||||
|
||||
requestedStatusID := c.Param(StatusIDKey)
|
||||
if requestedStatusID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id specified in request"})
|
||||
return
|
||||
}
|
||||
|
||||
// make sure this actually an AP request
|
||||
format := c.NegotiateFormat(ActivityPubAcceptHeaders...)
|
||||
if format == "" {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": "could not negotiate format with given Accept header(s)"})
|
||||
return
|
||||
}
|
||||
l.Tracef("negotiated format: %s", format)
|
||||
|
||||
// make a copy of the context to pass along so we don't break anything
|
||||
cp := c.Copy()
|
||||
status, err := m.processor.GetFediStatus(requestedUsername, requestedStatusID, cp.Request) // handles auth as well
|
||||
if err != nil {
|
||||
l.Info(err.Error())
|
||||
c.JSON(err.Code(), gin.H{"error": err.Safe()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, status)
|
||||
}
|
|
@ -32,6 +32,8 @@ import (
|
|||
const (
|
||||
// UsernameKey is for account usernames.
|
||||
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 = "/" + util.UsersPath
|
||||
// UsersBasePathWithUsername is just the users base path with the Username key in it.
|
||||
|
@ -40,6 +42,10 @@ const (
|
|||
UsersBasePathWithUsername = UsersBasePath + "/:" + UsernameKey
|
||||
// UsersInboxPath is for serving POST requests to a user's inbox with the given username key.
|
||||
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:
|
||||
|
@ -69,5 +75,7 @@ func New(config *config.Config, processor message.Processor, log *logrus.Logger)
|
|||
func (m *Module) Route(s router.Router) error {
|
||||
s.AttachHandler(http.MethodGet, UsersBasePathWithUsername, m.UsersGETHandler)
|
||||
s.AttachHandler(http.MethodPost, UsersInboxPath, m.InboxPOSTHandler)
|
||||
s.AttachHandler(http.MethodGet, UsersFollowersPath, m.FollowersGETHandler)
|
||||
s.AttachHandler(http.MethodGet, UsersStatusPath, m.StatusGETHandler)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -43,5 +43,6 @@ func New(config *config.Config, log *logrus.Logger) api.ClientModule {
|
|||
func (m *Module) Route(s router.Router) error {
|
||||
s.AttachMiddleware(m.FlocBlock)
|
||||
s.AttachMiddleware(m.ExtraHeaders)
|
||||
s.AttachMiddleware(m.UserAgentBlock)
|
||||
return nil
|
||||
}
|
||||
|
|
43
internal/api/security/useragentblock.go
Normal file
43
internal/api/security/useragentblock.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package security
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// UserAgentBlock is a middleware that prevents google chrome cohort tracking by
|
||||
// writing the Permissions-Policy header after all other parts of the request have been completed.
|
||||
// See: https://plausible.io/blog/google-floc
|
||||
func (m *Module) UserAgentBlock(c *gin.Context) {
|
||||
|
||||
ua := c.Request.UserAgent()
|
||||
if ua == "" {
|
||||
c.AbortWithStatus(http.StatusTeapot)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(strings.ToLower(c.Request.UserAgent()), strings.ToLower("friendica")) {
|
||||
c.AbortWithStatus(http.StatusTeapot)
|
||||
return
|
||||
}
|
||||
}
|
|
@ -22,7 +22,6 @@ import (
|
|||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/go-fed/activity/pub"
|
||||
"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.
|
||||
type ErrNoEntries struct{}
|
||||
|
||||
func (e ErrNoEntries) Error() string {
|
||||
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).
|
||||
// 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.
|
||||
type DB interface {
|
||||
// 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
|
||||
Federation() pub.Database
|
||||
// Federation() federatingdb.FederatingDB
|
||||
|
||||
/*
|
||||
BASIC DB FUNCTIONALITY
|
||||
|
@ -75,7 +84,7 @@ type DB interface {
|
|||
// 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.
|
||||
// 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".
|
||||
// // 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
|
||||
// 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
|
||||
|
@ -117,7 +126,9 @@ type DB interface {
|
|||
|
||||
// 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.
|
||||
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.
|
||||
// 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.
|
||||
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
|
||||
// privacy settings of the status, and any blocks/mutes that might exist between the two accounts
|
||||
// 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(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(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error)
|
||||
|
||||
|
|
|
@ -30,7 +30,6 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-fed/activity/pub"
|
||||
"github.com/go-pg/pg/extra/pgdebug"
|
||||
"github.com/go-pg/pg/v10"
|
||||
"github.com/go-pg/pg/v10/orm"
|
||||
|
@ -38,7 +37,6 @@ import (
|
|||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
@ -50,7 +48,7 @@ type postgresService struct {
|
|||
conn *pg.DB
|
||||
log *logrus.Logger
|
||||
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.
|
||||
|
@ -97,9 +95,6 @@ func NewPostgresService(ctx context.Context, c *config.Config, log *logrus.Logge
|
|||
cancel: cancel,
|
||||
}
|
||||
|
||||
federatingDB := federation.NewFederatingDB(ps, c, log)
|
||||
ps.federationDB = federatingDB
|
||||
|
||||
// we can confidently return this useable postgres service now
|
||||
return ps, nil
|
||||
}
|
||||
|
@ -159,14 +154,6 @@ func derivePGOptions(c *config.Config) (*pg.Options, error) {
|
|||
return options, nil
|
||||
}
|
||||
|
||||
/*
|
||||
FEDERATION FUNCTIONALITY
|
||||
*/
|
||||
|
||||
func (ps *postgresService) Federation() pub.Database {
|
||||
return ps.federationDB
|
||||
}
|
||||
|
||||
/*
|
||||
BASIC DB FUNCTIONALITY
|
||||
*/
|
||||
|
@ -229,8 +216,17 @@ func (ps *postgresService) GetByID(id string, i interface{}) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetWhere(key string, value interface{}, i interface{}) error {
|
||||
if err := ps.conn.Model(i).Where("? = ?", pg.Safe(key), value).Select(); err != nil {
|
||||
func (ps *postgresService) GetWhere(where []db.Where, i interface{}) error {
|
||||
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 {
|
||||
return db.ErrNoEntries{}
|
||||
}
|
||||
|
@ -255,6 +251,9 @@ func (ps *postgresService) GetAll(i interface{}) error {
|
|||
|
||||
func (ps *postgresService) Put(i interface{}) error {
|
||||
_, 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
|
||||
}
|
||||
|
||||
|
@ -285,21 +284,32 @@ func (ps *postgresService) UpdateOneByID(id string, key string, value interface{
|
|||
|
||||
func (ps *postgresService) DeleteByID(id string, i interface{}) error {
|
||||
if _, err := ps.conn.Model(i).Where("id = ?", id).Delete(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return db.ErrNoEntries{}
|
||||
}
|
||||
// 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 nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) DeleteWhere(key string, value interface{}, i interface{}) error {
|
||||
if _, err := ps.conn.Model(i).Where("? = ?", pg.Safe(key), value).Delete(); err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return db.ErrNoEntries{}
|
||||
func (ps *postgresService) DeleteWhere(where []db.Where, i interface{}) error {
|
||||
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.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 nil
|
||||
}
|
||||
|
||||
|
@ -307,30 +317,34 @@ func (ps *postgresService) DeleteWhere(key string, value interface{}, i interfac
|
|||
HANDY SHORTCUTS
|
||||
*/
|
||||
|
||||
func (ps *postgresService) AcceptFollowRequest(originAccountID string, targetAccountID string) error {
|
||||
func (ps *postgresService) AcceptFollowRequest(originAccountID string, targetAccountID string) (*gtsmodel.Follow, error) {
|
||||
// make sure the original follow request exists
|
||||
fr := >smodel.FollowRequest{}
|
||||
if err := ps.conn.Model(fr).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Select(); err != nil {
|
||||
if err == pg.ErrMultiRows {
|
||||
return db.ErrNoEntries{}
|
||||
return nil, db.ErrNoEntries{}
|
||||
}
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create a new follow to 'replace' the request with
|
||||
follow := >smodel.Follow{
|
||||
AccountID: originAccountID,
|
||||
TargetAccountID: targetAccountID,
|
||||
URI: fr.URI,
|
||||
}
|
||||
|
||||
if _, err := ps.conn.Model(follow).Insert(); err != nil {
|
||||
return err
|
||||
// if the follow already exists, just update the URI -- we don't need to do anything else
|
||||
if _, err := ps.conn.Model(follow).OnConflict("ON CONSTRAINT follows_account_id_target_account_id_key DO UPDATE set uri = ?", follow.URI).Insert(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// now remove the follow request
|
||||
if _, err := ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", originAccountID).Where("target_account_id = ?", targetAccountID).Delete(); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil
|
||||
return follow, nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) CreateInstanceAccount() error {
|
||||
|
@ -681,6 +695,60 @@ func (ps *postgresService) Blocked(account1 string, account2 string) (bool, erro
|
|||
return blocked, nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetRelationship(requestingAccount string, targetAccount string) (*gtsmodel.Relationship, error) {
|
||||
r := >smodel.Relationship{
|
||||
ID: targetAccount,
|
||||
}
|
||||
|
||||
// check if the requesting account follows the target account
|
||||
follow := >smodel.Follow{}
|
||||
if err := ps.conn.Model(follow).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Select(); err != nil {
|
||||
if err != pg.ErrNoRows {
|
||||
// a proper error
|
||||
return nil, fmt.Errorf("getrelationship: error checking follow existence: %s", err)
|
||||
}
|
||||
// no follow exists so these are all false
|
||||
r.Following = false
|
||||
r.ShowingReblogs = false
|
||||
r.Notifying = false
|
||||
} else {
|
||||
// follow exists so we can fill these fields out...
|
||||
r.Following = true
|
||||
r.ShowingReblogs = follow.ShowReblogs
|
||||
r.Notifying = follow.Notify
|
||||
}
|
||||
|
||||
// check if the target account follows the requesting account
|
||||
followedBy, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", targetAccount).Where("target_account_id = ?", requestingAccount).Exists()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getrelationship: error checking followed_by existence: %s", err)
|
||||
}
|
||||
r.FollowedBy = followedBy
|
||||
|
||||
// check if the requesting account blocks the target account
|
||||
blocking, err := ps.conn.Model(>smodel.Block{}).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Exists()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getrelationship: error checking blocking existence: %s", err)
|
||||
}
|
||||
r.Blocking = blocking
|
||||
|
||||
// check if the target account blocks the requesting account
|
||||
blockedBy, err := ps.conn.Model(>smodel.Block{}).Where("account_id = ?", targetAccount).Where("target_account_id = ?", requestingAccount).Exists()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getrelationship: error checking blocked existence: %s", err)
|
||||
}
|
||||
r.BlockedBy = blockedBy
|
||||
|
||||
// check if there's a pending following request from requesting account to target account
|
||||
requested, err := ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", requestingAccount).Where("target_account_id = ?", targetAccount).Exists()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getrelationship: error checking blocked existence: %s", err)
|
||||
}
|
||||
r.Requested = requested
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account, relevantAccounts *gtsmodel.RelevantAccounts) (bool, error) {
|
||||
l := ps.log.WithField("func", "StatusVisible")
|
||||
|
||||
|
@ -853,6 +921,10 @@ func (ps *postgresService) Follows(sourceAccount *gtsmodel.Account, targetAccoun
|
|||
return ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists()
|
||||
}
|
||||
|
||||
func (ps *postgresService) FollowRequested(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) {
|
||||
return ps.conn.Model(>smodel.FollowRequest{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists()
|
||||
}
|
||||
|
||||
func (ps *postgresService) Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error) {
|
||||
// make sure account 1 follows account 2
|
||||
f1, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", account1.ID).Where("target_account_id = ?", account2.ID).Exists()
|
||||
|
@ -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) {
|
||||
ogAccount := >smodel.Account{}
|
||||
if err := ps.conn.Model(ogAccount).Where("id = ?", originAccountID).Select(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
menchies := []*gtsmodel.Mention{}
|
||||
for _, a := range targetAccounts {
|
||||
// A mentioned account looks like "@test@example.org" or just "@test" for a local account
|
||||
|
@ -1094,8 +1171,12 @@ func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, ori
|
|||
// id, createdAt and updatedAt will be populated by the db, so we have everything we need!
|
||||
menchies = append(menchies, >smodel.Mention{
|
||||
StatusID: statusID,
|
||||
OriginAccountID: originAccountID,
|
||||
OriginAccountID: ogAccount.ID,
|
||||
OriginAccountURI: ogAccount.URI,
|
||||
TargetAccountID: mentionedAccount.ID,
|
||||
NameString: a,
|
||||
MentionedAccountURI: mentionedAccount.URI,
|
||||
GTSAccount: mentionedAccount,
|
||||
})
|
||||
}
|
||||
return menchies, nil
|
||||
|
|
|
@ -20,6 +20,7 @@ package federation
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
@ -37,6 +38,12 @@ import (
|
|||
"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.
|
||||
// It doesn't care what the underlying implementation of the DB interface is, as long as it works.
|
||||
type federatingDB struct {
|
||||
|
@ -47,8 +54,8 @@ type federatingDB struct {
|
|||
typeConverter typeutils.TypeConverter
|
||||
}
|
||||
|
||||
// NewFederatingDB returns a pub.Database interface using the given database, config, and logger.
|
||||
func NewFederatingDB(db db.DB, config *config.Config, log *logrus.Logger) pub.Database {
|
||||
// NewFederatingDB returns a FederatingDB interface using the given database, config, and logger.
|
||||
func NewFederatingDB(db db.DB, config *config.Config, log *logrus.Logger) FederatingDB {
|
||||
return &federatingDB{
|
||||
locks: new(sync.Map),
|
||||
db: db,
|
||||
|
@ -204,7 +211,7 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {
|
|||
if err != nil {
|
||||
return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err)
|
||||
}
|
||||
if err := f.db.GetWhere("uri", uid, >smodel.Status{}); err != nil {
|
||||
if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uid}}, >smodel.Status{}); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
// there are no entries for this status
|
||||
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())
|
||||
}
|
||||
acct := >smodel.Account{}
|
||||
if err := f.db.GetWhere("outbox_uri", outboxIRI.String(), acct); err != nil {
|
||||
if err := f.db.GetWhere([]db.Where{{Key: "outbox_uri", Value: outboxIRI.String()}}, acct); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
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())
|
||||
}
|
||||
acct := >smodel.Account{}
|
||||
if err := f.db.GetWhere("inbox_uri", inboxIRI.String(), acct); err != nil {
|
||||
if err := f.db.GetWhere([]db.Where{{Key: "inbox_uri", Value: inboxIRI.String()}}, acct); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
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())
|
||||
}
|
||||
acct := >smodel.Account{}
|
||||
if err := f.db.GetWhere("inbox_uri", inboxIRI.String(), acct); err != nil {
|
||||
if err := f.db.GetWhere([]db.Where{{Key: "inbox_uri", Value: inboxIRI.String()}}, acct); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
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) {
|
||||
acct := >smodel.Account{}
|
||||
if err := f.db.GetWhere("uri", id.String(), acct); err != nil {
|
||||
if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: id.String()}}, acct); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.Debug("is user path! returning account")
|
||||
return f.typeConverter.AccountToAS(acct)
|
||||
}
|
||||
|
||||
|
@ -371,27 +379,40 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
|
|||
"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)
|
||||
if targetAcctI == nil {
|
||||
l.Error("target account wasn't set on context")
|
||||
return nil
|
||||
}
|
||||
targetAcct, ok := targetAcctI.(*gtsmodel.Account)
|
||||
if !ok {
|
||||
l.Error("target account was set on context but couldn't be parsed")
|
||||
return nil
|
||||
}
|
||||
|
||||
fromFederatorChanI := ctx.Value(util.APFromFederatorChanKey)
|
||||
if fromFederatorChanI == nil {
|
||||
l.Error("from federator channel wasn't set on context")
|
||||
return nil
|
||||
}
|
||||
fromFederatorChan, ok := fromFederatorChanI.(chan gtsmodel.FromFederator)
|
||||
if !ok {
|
||||
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:
|
||||
create, ok := asType.(vocab.ActivityStreamsCreate)
|
||||
if !ok {
|
||||
|
@ -399,7 +420,7 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
|
|||
}
|
||||
object := create.GetActivityStreamsObject()
|
||||
for objectIter := object.Begin(); objectIter != object.End(); objectIter = objectIter.Next() {
|
||||
switch gtsmodel.ActivityStreamsObject(objectIter.GetType().GetTypeName()) {
|
||||
switch objectIter.GetType().GetTypeName() {
|
||||
case gtsmodel.ActivityStreamsNote:
|
||||
note := objectIter.GetActivityStreamsNote()
|
||||
status, err := f.typeConverter.ASStatusToStatus(note)
|
||||
|
@ -407,6 +428,9 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
|
|||
return fmt.Errorf("error converting note to status: %s", err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -414,6 +438,7 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
|
|||
APObjectType: gtsmodel.ActivityStreamsNote,
|
||||
APActivityType: gtsmodel.ActivityStreamsCreate,
|
||||
GTSModel: status,
|
||||
ReceivingAccount: targetAcct,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -433,7 +458,7 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -450,14 +475,87 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
|
|||
// the entire value.
|
||||
//
|
||||
// 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(
|
||||
logrus.Fields{
|
||||
"func": "Update",
|
||||
"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
|
||||
}
|
||||
|
||||
|
@ -490,7 +588,7 @@ func (f *federatingDB) GetOutbox(c context.Context, outboxIRI *url.URL) (inbox v
|
|||
)
|
||||
l.Debug("entering GETOUTBOX function")
|
||||
|
||||
return nil, nil
|
||||
return streams.NewActivityStreamsOrderedCollectionPage(), nil
|
||||
}
|
||||
|
||||
// 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(),
|
||||
},
|
||||
)
|
||||
l.Debugf("received NEWID request for asType %+v", t)
|
||||
m, err := streams.Serialize(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.Debugf("received NEWID request for asType %s", string(b))
|
||||
|
||||
return url.Parse(fmt.Sprintf("%s://%s/", f.config.Protocol, uuid.NewString()))
|
||||
switch t.GetTypeName() {
|
||||
case gtsmodel.ActivityStreamsFollow:
|
||||
// FOLLOW
|
||||
// ID might already be set on a follow we've created, so check it here and return it if it is
|
||||
follow, ok := t.(vocab.ActivityStreamsFollow)
|
||||
if !ok {
|
||||
return nil, errors.New("newid: follow couldn't be parsed into vocab.ActivityStreamsFollow")
|
||||
}
|
||||
idProp := follow.GetJSONLDId()
|
||||
if idProp != nil {
|
||||
if idProp.IsIRI() {
|
||||
return idProp.GetIRI(), nil
|
||||
}
|
||||
}
|
||||
// it's not set so create one based on the actor set on the follow (ie., the followER not the followEE)
|
||||
actorProp := follow.GetActivityStreamsActor()
|
||||
if actorProp != nil {
|
||||
for iter := actorProp.Begin(); iter != actorProp.End(); iter = iter.Next() {
|
||||
// take the IRI of the first actor we can find (there should only be one)
|
||||
if iter.IsIRI() {
|
||||
actorAccount := >smodel.Account{}
|
||||
if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: iter.GetIRI().String()}}, actorAccount); err == nil { // if there's an error here, just use the fallback behavior -- we don't need to return an error here
|
||||
return url.Parse(util.GenerateURIForFollow(actorAccount.Username, f.config.Protocol, f.config.Host))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case gtsmodel.ActivityStreamsNote:
|
||||
// NOTE aka STATUS
|
||||
// ID might already be set on a note we've created, so check it here and return it if it is
|
||||
note, ok := t.(vocab.ActivityStreamsNote)
|
||||
if !ok {
|
||||
return nil, errors.New("newid: follow couldn't be parsed into vocab.ActivityStreamsNote")
|
||||
}
|
||||
idProp := note.GetJSONLDId()
|
||||
if idProp != nil {
|
||||
if idProp.IsIRI() {
|
||||
return idProp.GetIRI(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fallback default behavior: just return a random UUID after our protocol and host
|
||||
return url.Parse(fmt.Sprintf("%s://%s/%s", f.config.Protocol, f.config.Host, uuid.NewString()))
|
||||
}
|
||||
|
||||
// Followers obtains the Followers Collection for an actor with the
|
||||
|
@ -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())
|
||||
|
||||
acct := >smodel.Account{}
|
||||
if err := f.db.GetWhere("uri", actorIRI.String(), acct); err != nil {
|
||||
if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: actorIRI.String()}}, acct); err != nil {
|
||||
return nil, fmt.Errorf("db error getting account with uri %s: %s", actorIRI.String(), err)
|
||||
}
|
||||
|
||||
|
@ -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())
|
||||
|
||||
acct := >smodel.Account{}
|
||||
if err := f.db.GetWhere("uri", actorIRI.String(), acct); err != nil {
|
||||
if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: actorIRI.String()}}, acct); err != nil {
|
||||
return nil, fmt.Errorf("db error getting account with uri %s: %s", actorIRI.String(), err)
|
||||
}
|
||||
|
||||
|
@ -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())
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
/*
|
||||
CUSTOM FUNCTIONALITY FOR GTS
|
||||
*/
|
||||
|
||||
func (f *federatingDB) Undo(ctx context.Context, undo vocab.ActivityStreamsUndo) error {
|
||||
l := f.log.WithFields(
|
||||
logrus.Fields{
|
||||
"func": "Undo",
|
||||
"asType": undo.GetTypeName(),
|
||||
},
|
||||
)
|
||||
m, err := streams.Serialize(undo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.Debugf("received UNDO asType %s", string(b))
|
||||
|
||||
targetAcctI := ctx.Value(util.APAccount)
|
||||
if targetAcctI == nil {
|
||||
l.Error("UNDO: target account wasn't set on context")
|
||||
return nil
|
||||
}
|
||||
targetAcct, ok := targetAcctI.(*gtsmodel.Account)
|
||||
if !ok {
|
||||
l.Error("UNDO: target account was set on context but couldn't be parsed")
|
||||
return nil
|
||||
}
|
||||
|
||||
undoObject := undo.GetActivityStreamsObject()
|
||||
if undoObject == nil {
|
||||
return errors.New("UNDO: no object set on vocab.ActivityStreamsUndo")
|
||||
}
|
||||
|
||||
for iter := undoObject.Begin(); iter != undoObject.End(); iter = iter.Next() {
|
||||
switch iter.GetType().GetTypeName() {
|
||||
case string(gtsmodel.ActivityStreamsFollow):
|
||||
// UNDO FOLLOW
|
||||
ASFollow, ok := iter.GetType().(vocab.ActivityStreamsFollow)
|
||||
if !ok {
|
||||
return errors.New("UNDO: couldn't parse follow into vocab.ActivityStreamsFollow")
|
||||
}
|
||||
// make sure the actor owns the follow
|
||||
if !sameActor(undo.GetActivityStreamsActor(), ASFollow.GetActivityStreamsActor()) {
|
||||
return errors.New("UNDO: follow actor and activity actor not the same")
|
||||
}
|
||||
// convert the follow to something we can understand
|
||||
gtsFollow, err := f.typeConverter.ASFollowToFollow(ASFollow)
|
||||
if err != nil {
|
||||
return fmt.Errorf("UNDO: error converting asfollow to gtsfollow: %s", err)
|
||||
}
|
||||
// make sure the addressee of the original follow is the same as whatever inbox this landed in
|
||||
if gtsFollow.TargetAccountID != targetAcct.ID {
|
||||
return errors.New("UNDO: follow object account and inbox account were not the same")
|
||||
}
|
||||
// delete any existing FOLLOW
|
||||
if err := f.db.DeleteWhere([]db.Where{{Key: "uri", Value: gtsFollow.URI}}, >smodel.Follow{}); err != nil {
|
||||
return fmt.Errorf("UNDO: db error removing follow: %s", err)
|
||||
}
|
||||
// delete any existing FOLLOW REQUEST
|
||||
if err := f.db.DeleteWhere([]db.Where{{Key: "uri", Value: gtsFollow.URI}}, >smodel.FollowRequest{}); err != nil {
|
||||
return fmt.Errorf("UNDO: db error removing follow request: %s", err)
|
||||
}
|
||||
l.Debug("follow undone")
|
||||
return nil
|
||||
case string(gtsmodel.ActivityStreamsLike):
|
||||
// UNDO LIKE
|
||||
case string(gtsmodel.ActivityStreamsAnnounce):
|
||||
// UNDO BOOST/REBLOG/ANNOUNCE
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) error {
|
||||
l := f.log.WithFields(
|
||||
logrus.Fields{
|
||||
"func": "Accept",
|
||||
"asType": accept.GetTypeName(),
|
||||
},
|
||||
)
|
||||
m, err := streams.Serialize(accept)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.Debugf("received ACCEPT asType %s", string(b))
|
||||
|
||||
inboxAcctI := ctx.Value(util.APAccount)
|
||||
if inboxAcctI == nil {
|
||||
l.Error("ACCEPT: inbox account wasn't set on context")
|
||||
return nil
|
||||
}
|
||||
inboxAcct, ok := inboxAcctI.(*gtsmodel.Account)
|
||||
if !ok {
|
||||
l.Error("ACCEPT: inbox account was set on context but couldn't be parsed")
|
||||
return nil
|
||||
}
|
||||
|
||||
acceptObject := accept.GetActivityStreamsObject()
|
||||
if acceptObject == nil {
|
||||
return errors.New("ACCEPT: no object set on vocab.ActivityStreamsUndo")
|
||||
}
|
||||
|
||||
for iter := acceptObject.Begin(); iter != acceptObject.End(); iter = iter.Next() {
|
||||
switch iter.GetType().GetTypeName() {
|
||||
case string(gtsmodel.ActivityStreamsFollow):
|
||||
// ACCEPT FOLLOW
|
||||
asFollow, ok := iter.GetType().(vocab.ActivityStreamsFollow)
|
||||
if !ok {
|
||||
return errors.New("ACCEPT: couldn't parse follow into vocab.ActivityStreamsFollow")
|
||||
}
|
||||
// convert the follow to something we can understand
|
||||
gtsFollow, err := f.typeConverter.ASFollowToFollow(asFollow)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ACCEPT: error converting asfollow to gtsfollow: %s", err)
|
||||
}
|
||||
// make sure the addressee of the original follow is the same as whatever inbox this landed in
|
||||
if gtsFollow.AccountID != inboxAcct.ID {
|
||||
return errors.New("ACCEPT: follow object account and inbox account were not the same")
|
||||
}
|
||||
_, err = f.db.AcceptFollowRequest(gtsFollow.AccountID, gtsFollow.TargetAccountID)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -124,7 +124,7 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr
|
|||
}
|
||||
|
||||
requestingAccount := >smodel.Account{}
|
||||
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
|
||||
if _, ok := err.(db.ErrNoEntries); !ok {
|
||||
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
|
||||
|
||||
// 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)
|
||||
|
@ -184,7 +200,7 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er
|
|||
|
||||
for _, uri := range actorIRIs {
|
||||
a := >smodel.Account{}
|
||||
if err := f.db.GetWhere("uri", uri.String(), a); err != nil {
|
||||
if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, a); err != nil {
|
||||
_, ok := err.(db.ErrNoEntries)
|
||||
if ok {
|
||||
// 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",
|
||||
})
|
||||
|
||||
targetAcctI := ctx.Value(util.APAccount)
|
||||
if targetAcctI == nil {
|
||||
l.Error("target account wasn't set on context")
|
||||
receivingAcctI := ctx.Value(util.APAccount)
|
||||
if receivingAcctI == nil {
|
||||
l.Error("receiving account wasn't set on context")
|
||||
return
|
||||
}
|
||||
targetAcct, ok := targetAcctI.(*gtsmodel.Account)
|
||||
receivingAcct, ok := receivingAcctI.(*gtsmodel.Account)
|
||||
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
|
||||
if targetAcct.Locked {
|
||||
if receivingAcct.Locked {
|
||||
onFollow = pub.OnFollowDoNothing
|
||||
}
|
||||
|
||||
|
@ -248,6 +266,17 @@ func (f *federator) FederatingCallbacks(ctx context.Context) (wrapped pub.Federa
|
|||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -34,6 +34,8 @@ import (
|
|||
type Federator interface {
|
||||
// FederatingActor returns the underlying pub.FederatingActor, which can be used to send activities, and serve actors at inboxes/outboxes.
|
||||
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.
|
||||
// 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)
|
||||
|
@ -52,6 +54,7 @@ type Federator interface {
|
|||
type federator struct {
|
||||
config *config.Config
|
||||
db db.DB
|
||||
federatingDB FederatingDB
|
||||
clock pub.Clock
|
||||
typeConverter typeutils.TypeConverter
|
||||
transportController transport.Controller
|
||||
|
@ -60,18 +63,19 @@ type federator struct {
|
|||
}
|
||||
|
||||
// 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{}
|
||||
f := &federator{
|
||||
config: config,
|
||||
db: db,
|
||||
federatingDB: federatingDB,
|
||||
clock: &Clock{},
|
||||
typeConverter: typeConverter,
|
||||
transportController: transportController,
|
||||
log: log,
|
||||
}
|
||||
actor := newFederatingActor(f, f, db.Federation(), clock)
|
||||
actor := newFederatingActor(f, f, federatingDB, clock)
|
||||
f.actor = actor
|
||||
return f
|
||||
}
|
||||
|
@ -79,3 +83,7 @@ func NewFederator(db db.DB, transportController transport.Controller, config *co
|
|||
func (f *federator) FederatingActor() pub.FederatingActor {
|
||||
return f.actor
|
||||
}
|
||||
|
||||
func (f *federator) FederatingDB() FederatingDB {
|
||||
return f.federatingDB
|
||||
}
|
||||
|
|
|
@ -89,7 +89,7 @@ func (suite *ProtocolTestSuite) TestPostInboxRequestBodyHook() {
|
|||
return nil, nil
|
||||
}))
|
||||
// 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
|
||||
ctx := context.Background()
|
||||
|
@ -155,7 +155,7 @@ func (suite *ProtocolTestSuite) TestAuthenticatePostInbox() {
|
|||
}))
|
||||
|
||||
// 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
|
||||
ctx := context.Background()
|
||||
|
|
|
@ -27,11 +27,13 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/go-fed/activity/pub"
|
||||
"github.com/go-fed/activity/streams"
|
||||
"github.com/go-fed/activity/streams/vocab"
|
||||
"github.com/go-fed/httpsig"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
|
@ -128,6 +130,21 @@ func (f *federator) AuthenticateFederatedRequest(username string, r *http.Reques
|
|||
return nil, fmt.Errorf("could not parse key id into a url: %s", err)
|
||||
}
|
||||
|
||||
var publicKey interface{}
|
||||
var pkOwnerURI *url.URL
|
||||
if strings.EqualFold(requestingPublicKeyID.Host, f.config.Host) {
|
||||
// the request is coming from INSIDE THE HOUSE so skip the remote dereferencing
|
||||
requestingLocalAccount := >smodel.Account{}
|
||||
if err := f.db.GetWhere([]db.Where{{Key: "public_key_uri", Value: requestingPublicKeyID.String()}}, requestingLocalAccount); err != nil {
|
||||
return nil, fmt.Errorf("couldn't get local account with public key uri %s from the database: %s", requestingPublicKeyID.String(), err)
|
||||
}
|
||||
publicKey = requestingLocalAccount.PublicKey
|
||||
pkOwnerURI, err = url.Parse(requestingLocalAccount.URI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing url %s: %s", requestingLocalAccount.URI, err)
|
||||
}
|
||||
} else {
|
||||
// the request is remote, so we need to authenticate the request properly by dereferencing the remote key
|
||||
transport, err := f.GetTransportForUser(username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("transport err: %s", err)
|
||||
|
@ -158,26 +175,27 @@ func (f *federator) AuthenticateFederatedRequest(username string, r *http.Reques
|
|||
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 {
|
||||
return nil, fmt.Errorf("could not parse public key from block bytes: %s", err)
|
||||
}
|
||||
if p == nil {
|
||||
return nil, errors.New("returned public key was empty")
|
||||
}
|
||||
|
||||
// do the actual authentication here!
|
||||
algo := httpsig.RSA_SHA256 // TODO: make this more robust
|
||||
if err := verifier.Verify(p, algo); err != nil {
|
||||
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()
|
||||
pkOwnerURI = pkOwnerProp.GetIRI()
|
||||
}
|
||||
if publicKey == nil {
|
||||
return nil, errors.New("returned public key was empty")
|
||||
}
|
||||
|
||||
// do the actual authentication here!
|
||||
algo := httpsig.RSA_SHA256 // TODO: make this more robust
|
||||
if err := verifier.Verify(publicKey, algo); err != nil {
|
||||
return nil, fmt.Errorf("error verifying key %s: %s", requestingPublicKeyID.String(), err)
|
||||
}
|
||||
|
||||
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 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())
|
||||
|
@ -243,3 +267,23 @@ func (f *federator) GetTransportForUser(username string) (transport.Transport, e
|
|||
}
|
||||
return transport, nil
|
||||
}
|
||||
|
||||
func sameActor(activityActor vocab.ActivityStreamsActorProperty, followActor vocab.ActivityStreamsActorProperty) bool {
|
||||
if activityActor == nil || followActor == nil {
|
||||
return false
|
||||
}
|
||||
for aIter := activityActor.Begin(); aIter != activityActor.End(); aIter = aIter.Next() {
|
||||
for fIter := followActor.Begin(); fIter != followActor.End(); fIter = fIter.Next() {
|
||||
if aIter.GetIRI() == nil {
|
||||
return false
|
||||
}
|
||||
if fIter.GetIRI() == nil {
|
||||
return false
|
||||
}
|
||||
if aIter.GetIRI().String() == fIter.GetIRI().String() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -83,6 +83,8 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr
|
|||
return fmt.Errorf("error creating dbservice: %s", err)
|
||||
}
|
||||
|
||||
federatingDB := federation.NewFederatingDB(dbService, c, log)
|
||||
|
||||
router, err := router.New(c, log)
|
||||
if err != nil {
|
||||
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)
|
||||
oauthServer := oauth.New(dbService, 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)
|
||||
if err := processor.Start(); err != nil {
|
||||
return fmt.Errorf("error starting processor: %s", err)
|
||||
|
|
|
@ -76,13 +76,13 @@ type Account struct {
|
|||
*/
|
||||
|
||||
// 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?
|
||||
Discoverable bool
|
||||
// Default post privacy for this account
|
||||
Privacy Visibility
|
||||
// Set posts from this account to sensitive by default?
|
||||
Sensitive bool `pg:",default:'false'"`
|
||||
Sensitive bool
|
||||
// What language does this account post in?
|
||||
Language string `pg:",default:'en'"`
|
||||
|
||||
|
@ -107,7 +107,7 @@ type Account struct {
|
|||
// URL for getting the featured collection list of this account
|
||||
FeaturedCollectionURI string `pg:",unique"`
|
||||
// What type of activitypub actor is this account?
|
||||
ActorType ActivityStreamsActor
|
||||
ActorType string
|
||||
// This account is associated with x account id
|
||||
AlsoKnownAs string
|
||||
|
||||
|
|
|
@ -18,110 +18,101 @@
|
|||
|
||||
package gtsmodel
|
||||
|
||||
// ActivityStreamsObject refers to https://www.w3.org/TR/activitystreams-vocabulary/#object-types
|
||||
type ActivityStreamsObject string
|
||||
|
||||
const (
|
||||
// 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 ActivityStreamsObject = "Audio"
|
||||
ActivityStreamsAudio = "Audio"
|
||||
// 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 ActivityStreamsObject = "Event"
|
||||
ActivityStreamsEvent = "Event"
|
||||
// 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 ActivityStreamsObject = "Note"
|
||||
ActivityStreamsNote = "Note"
|
||||
// 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 ActivityStreamsObject = "Place"
|
||||
ActivityStreamsPlace = "Place"
|
||||
// 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 ActivityStreamsObject = "Relationship"
|
||||
ActivityStreamsRelationship = "Relationship"
|
||||
// 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 ActivityStreamsObject = "Video"
|
||||
ActivityStreamsVideo = "Video"
|
||||
)
|
||||
|
||||
// ActivityStreamsActor refers to https://www.w3.org/TR/activitystreams-vocabulary/#actor-types
|
||||
type ActivityStreamsActor string
|
||||
|
||||
const (
|
||||
// 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 ActivityStreamsActor = "Group"
|
||||
ActivityStreamsGroup = "Group"
|
||||
// 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 ActivityStreamsActor = "Person"
|
||||
ActivityStreamsPerson = "Person"
|
||||
// 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 (
|
||||
// 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 ActivityStreamsActivity = "Add"
|
||||
ActivityStreamsAdd = "Add"
|
||||
// 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 ActivityStreamsActivity = "Arrive"
|
||||
ActivityStreamsArrive = "Arrive"
|
||||
// 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 ActivityStreamsActivity = "Create"
|
||||
ActivityStreamsCreate = "Create"
|
||||
// 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 ActivityStreamsActivity = "Dislike"
|
||||
ActivityStreamsDislike = "Dislike"
|
||||
// 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 ActivityStreamsActivity = "Follow"
|
||||
ActivityStreamsFollow = "Follow"
|
||||
// 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 ActivityStreamsActivity = "Invite"
|
||||
ActivityStreamsInvite = "Invite"
|
||||
// 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 ActivityStreamsActivity = "Leave"
|
||||
ActivityStreamsLeave = "Leave"
|
||||
// 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 ActivityStreamsActivity = "Listen"
|
||||
ActivityStreamsListen = "Listen"
|
||||
// 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 ActivityStreamsActivity = "Offer"
|
||||
ActivityStreamsOffer = "Offer"
|
||||
// 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 ActivityStreamsActivity = "Reject"
|
||||
ActivityStreamsReject = "Reject"
|
||||
// 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 ActivityStreamsActivity = "Remove"
|
||||
ActivityStreamsRemove = "Remove"
|
||||
// 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 ActivityStreamsActivity = "TentativeAccept"
|
||||
ActivityStreamsTentativeAccept = "TentativeAccept"
|
||||
// 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 ActivityStreamsActivity = "Undo"
|
||||
ActivityStreamsUndo = "Undo"
|
||||
// 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 ActivityStreamsActivity = "View"
|
||||
ActivityStreamsView = "View"
|
||||
)
|
||||
|
|
|
@ -38,6 +38,14 @@ type Mention struct {
|
|||
TargetAccountID string `pg:",notnull"`
|
||||
// Prevent this mention from generating a notification?
|
||||
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
|
||||
// before the mention is dereferenced. Should be in a form along the lines of:
|
||||
// @whatever_username@example.org
|
||||
|
@ -48,4 +56,6 @@ type Mention struct {
|
|||
//
|
||||
// This will not be put in the database, it's just for convenience.
|
||||
MentionedAccountURI string `pg:"-"`
|
||||
// A pointer to the gtsmodel account of the mentioned account.
|
||||
GTSAccount *Account `pg:"-"`
|
||||
}
|
||||
|
|
|
@ -9,9 +9,11 @@ package gtsmodel
|
|||
|
||||
// FromClientAPI wraps a message that travels from client API into the processor
|
||||
type FromClientAPI struct {
|
||||
APObjectType ActivityStreamsObject
|
||||
APActivityType ActivityStreamsActivity
|
||||
APObjectType string
|
||||
APActivityType string
|
||||
GTSModel interface{}
|
||||
OriginAccount *Account
|
||||
TargetAccount *Account
|
||||
}
|
||||
|
||||
// // 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
|
||||
type FromFederator struct {
|
||||
APObjectType ActivityStreamsObject
|
||||
APActivityType ActivityStreamsActivity
|
||||
APObjectType string
|
||||
APActivityType string
|
||||
GTSModel interface{}
|
||||
ReceivingAccount *Account
|
||||
}
|
||||
|
|
49
internal/gtsmodel/relationship.go
Normal file
49
internal/gtsmodel/relationship.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package gtsmodel
|
||||
|
||||
// Relationship describes a requester's relationship with another account.
|
||||
type Relationship struct {
|
||||
// The account id.
|
||||
ID string
|
||||
// Are you following this user?
|
||||
Following bool
|
||||
// Are you receiving this user's boosts in your home timeline?
|
||||
ShowingReblogs bool
|
||||
// Have you enabled notifications for this user?
|
||||
Notifying bool
|
||||
// Are you followed by this user?
|
||||
FollowedBy bool
|
||||
// Are you blocking this user?
|
||||
Blocking bool
|
||||
// Is this user blocking you?
|
||||
BlockedBy bool
|
||||
// Are you muting this user?
|
||||
Muting bool
|
||||
// Are you muting notifications from this user?
|
||||
MutingNotifications bool
|
||||
// Do you have a pending follow request for this user?
|
||||
Requested bool
|
||||
// Are you blocking this user's domain?
|
||||
DomainBlocking bool
|
||||
// Are you featuring this user on your profile?
|
||||
Endorsed bool
|
||||
// Your note on this account.
|
||||
Note string
|
||||
}
|
|
@ -66,7 +66,7 @@ type Status struct {
|
|||
VisibilityAdvanced *VisibilityAdvanced
|
||||
// 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!.
|
||||
ActivityStreamsType ActivityStreamsObject
|
||||
ActivityStreamsType string
|
||||
// Original text of the status without formatting
|
||||
Text string
|
||||
// Has this status been pinned by its owner?
|
||||
|
|
|
@ -67,7 +67,7 @@ type Handler interface {
|
|||
// ProcessHeaderOrAvatar takes a new header image for an account, checks it out, removes exif data from it,
|
||||
// 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.
|
||||
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,
|
||||
// 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
|
||||
// in the database.
|
||||
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 {
|
||||
|
@ -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,
|
||||
// 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.
|
||||
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")
|
||||
|
||||
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))
|
||||
|
||||
// process it
|
||||
ma, err := mh.processHeaderOrAvi(attachment, contentType, mediaType, accountID)
|
||||
ma, err := mh.processHeaderOrAvi(attachment, contentType, mediaType, accountID, remoteURL)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
func (mh *mediaHandler) ProcessRemoteHeaderOrAvatar(t transport.Transport, currentAttachment *gtsmodel.MediaAttachment, accountID string) (*gtsmodel.MediaAttachment, error) {
|
||||
|
||||
if !currentAttachment.Header && !currentAttachment.Avatar {
|
||||
return nil, errors.New("provided attachment was set to neither header nor avatar")
|
||||
}
|
||||
|
||||
if currentAttachment.Header && currentAttachment.Avatar {
|
||||
return nil, errors.New("provided attachment was set to both header and avatar")
|
||||
}
|
||||
|
||||
var headerOrAvi Type
|
||||
if currentAttachment.Header {
|
||||
headerOrAvi = Header
|
||||
} else if currentAttachment.Avatar {
|
||||
headerOrAvi = Avatar
|
||||
}
|
||||
|
||||
if currentAttachment.RemoteURL == "" {
|
||||
return nil, errors.New("no remote URL on media attachment to dereference")
|
||||
}
|
||||
remoteIRI, err := url.Parse(currentAttachment.RemoteURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing attachment url %s: %s", currentAttachment.RemoteURL, err)
|
||||
}
|
||||
|
||||
// for content type, we assume we don't know what to expect...
|
||||
expectedContentType := "*/*"
|
||||
if currentAttachment.File.ContentType != "" {
|
||||
// ... and then narrow it down if we do
|
||||
expectedContentType = currentAttachment.File.ContentType
|
||||
}
|
||||
|
||||
attachmentBytes, err := t.DereferenceMedia(context.Background(), remoteIRI, expectedContentType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dereferencing remote media with url %s: %s", remoteIRI.String(), err)
|
||||
}
|
||||
|
||||
return mh.ProcessHeaderOrAvatar(attachmentBytes, accountID, headerOrAvi, currentAttachment.RemoteURL)
|
||||
}
|
||||
|
|
|
@ -147,7 +147,7 @@ func (suite *MediaTestSuite) TestSetHeaderOrAvatarForAccountID() {
|
|||
f, err := ioutil.ReadFile("./test/test-jpeg.jpg")
|
||||
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)
|
||||
suite.log.Debugf("%+v", ma)
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ import (
|
|||
"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 isAvatar bool
|
||||
|
||||
|
@ -96,7 +96,7 @@ func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string
|
|||
ID: newMediaID,
|
||||
StatusID: "",
|
||||
URL: originalURL,
|
||||
RemoteURL: "",
|
||||
RemoteURL: remoteURL,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
Type: gtsmodel.FileTypeImage,
|
||||
|
|
|
@ -78,6 +78,15 @@ func (p *processor) AccountGet(authed *oauth.Auth, targetAccountID string) (*api
|
|||
return nil, fmt.Errorf("db error: %s", err)
|
||||
}
|
||||
|
||||
// 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 err error
|
||||
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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, NewErrorInternalError(err)
|
||||
|
@ -293,3 +308,238 @@ func (p *processor) AccountFollowersGet(authed *oauth.Auth, targetAccountID stri
|
|||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
func (p *processor) AccountFollowingGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode) {
|
||||
blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID)
|
||||
if err != nil {
|
||||
return nil, NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if blocked {
|
||||
return nil, NewErrorNotFound(fmt.Errorf("block exists between accounts"))
|
||||
}
|
||||
|
||||
following := []gtsmodel.Follow{}
|
||||
accounts := []apimodel.Account{}
|
||||
if err := p.db.GetFollowingByAccountID(targetAccountID, &following); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
return accounts, nil
|
||||
}
|
||||
return nil, NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
for _, f := range following {
|
||||
blocked, err := p.db.Blocked(authed.Account.ID, f.AccountID)
|
||||
if err != nil {
|
||||
return nil, NewErrorInternalError(err)
|
||||
}
|
||||
if blocked {
|
||||
continue
|
||||
}
|
||||
|
||||
a := >smodel.Account{}
|
||||
if err := p.db.GetByID(f.TargetAccountID, a); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
continue
|
||||
}
|
||||
return nil, NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// derefence account fields in case we haven't done it already
|
||||
if err := p.dereferenceAccountFields(a, authed.Account.Username); err != nil {
|
||||
// don't bail if we can't fetch them, we'll try another time
|
||||
p.log.WithField("func", "AccountFollowingGet").Debugf("error dereferencing account fields: %s", err)
|
||||
}
|
||||
|
||||
account, err := p.tc.AccountToMastoPublic(a)
|
||||
if err != nil {
|
||||
return nil, NewErrorInternalError(err)
|
||||
}
|
||||
accounts = append(accounts, *account)
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
func (p *processor) AccountRelationshipGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode) {
|
||||
if authed == nil || authed.Account == nil {
|
||||
return nil, NewErrorForbidden(errors.New("not authed"))
|
||||
}
|
||||
|
||||
gtsR, err := p.db.GetRelationship(authed.Account.ID, targetAccountID)
|
||||
if err != nil {
|
||||
return nil, NewErrorInternalError(fmt.Errorf("error getting relationship: %s", err))
|
||||
}
|
||||
|
||||
r, err := p.tc.RelationshipToMasto(gtsR)
|
||||
if err != nil {
|
||||
return nil, NewErrorInternalError(fmt.Errorf("error converting relationship: %s", err))
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (p *processor) AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, ErrorWithCode) {
|
||||
// if there's a block between the accounts we shouldn't create the request ofc
|
||||
blocked, err := p.db.Blocked(authed.Account.ID, form.TargetAccountID)
|
||||
if err != nil {
|
||||
return nil, NewErrorInternalError(err)
|
||||
}
|
||||
if blocked {
|
||||
return nil, NewErrorNotFound(fmt.Errorf("accountfollowcreate: block exists between accounts"))
|
||||
}
|
||||
|
||||
// make sure the target account actually exists in our db
|
||||
targetAcct := >smodel.Account{}
|
||||
if err := p.db.GetByID(form.TargetAccountID, targetAcct); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
return nil, NewErrorNotFound(fmt.Errorf("accountfollowcreate: account %s not found in the db: %s", form.TargetAccountID, err))
|
||||
}
|
||||
}
|
||||
|
||||
// check if a follow exists already
|
||||
follows, err := p.db.Follows(authed.Account, targetAcct)
|
||||
if err != nil {
|
||||
return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow in db: %s", err))
|
||||
}
|
||||
if follows {
|
||||
// already follows so just return the relationship
|
||||
return p.AccountRelationshipGet(authed, form.TargetAccountID)
|
||||
}
|
||||
|
||||
// check if a follow exists already
|
||||
followRequested, err := p.db.FollowRequested(authed.Account, targetAcct)
|
||||
if err != nil {
|
||||
return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow request in db: %s", err))
|
||||
}
|
||||
if followRequested {
|
||||
// already follow requested so just return the relationship
|
||||
return p.AccountRelationshipGet(authed, form.TargetAccountID)
|
||||
}
|
||||
|
||||
// make the follow request
|
||||
fr := >smodel.FollowRequest{
|
||||
AccountID: authed.Account.ID,
|
||||
TargetAccountID: form.TargetAccountID,
|
||||
ShowReblogs: true,
|
||||
URI: util.GenerateURIForFollow(authed.Account.Username, p.config.Protocol, p.config.Host),
|
||||
Notify: false,
|
||||
}
|
||||
if form.Reblogs != nil {
|
||||
fr.ShowReblogs = *form.Reblogs
|
||||
}
|
||||
if form.Notify != nil {
|
||||
fr.Notify = *form.Notify
|
||||
}
|
||||
|
||||
// whack it in the database
|
||||
if err := p.db.Put(fr); err != nil {
|
||||
return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error creating follow request in db: %s", err))
|
||||
}
|
||||
|
||||
// if it's a local account that's not locked we can just straight up accept the follow request
|
||||
if !targetAcct.Locked && targetAcct.Domain == "" {
|
||||
if _, err := p.db.AcceptFollowRequest(authed.Account.ID, form.TargetAccountID); err != nil {
|
||||
return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error accepting folow request for local unlocked account: %s", err))
|
||||
}
|
||||
// return the new relationship
|
||||
return p.AccountRelationshipGet(authed, form.TargetAccountID)
|
||||
}
|
||||
|
||||
// otherwise we leave the follow request as it is and we handle the rest of the process asynchronously
|
||||
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
||||
APObjectType: gtsmodel.ActivityStreamsFollow,
|
||||
APActivityType: gtsmodel.ActivityStreamsCreate,
|
||||
GTSModel: >smodel.Follow{
|
||||
AccountID: authed.Account.ID,
|
||||
TargetAccountID: form.TargetAccountID,
|
||||
URI: fr.URI,
|
||||
},
|
||||
OriginAccount: authed.Account,
|
||||
TargetAccount: targetAcct,
|
||||
}
|
||||
|
||||
// return whatever relationship results from this
|
||||
return p.AccountRelationshipGet(authed, form.TargetAccountID)
|
||||
}
|
||||
|
||||
func (p *processor) AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode) {
|
||||
// if there's a block between the accounts we shouldn't do anything
|
||||
blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID)
|
||||
if err != nil {
|
||||
return nil, NewErrorInternalError(err)
|
||||
}
|
||||
if blocked {
|
||||
return nil, NewErrorNotFound(fmt.Errorf("AccountFollowRemove: block exists between accounts"))
|
||||
}
|
||||
|
||||
// make sure the target account actually exists in our db
|
||||
targetAcct := >smodel.Account{}
|
||||
if err := p.db.GetByID(targetAccountID, targetAcct); err != nil {
|
||||
if _, ok := err.(db.ErrNoEntries); ok {
|
||||
return nil, NewErrorNotFound(fmt.Errorf("AccountFollowRemove: account %s not found in the db: %s", targetAccountID, err))
|
||||
}
|
||||
}
|
||||
|
||||
// check if a follow request exists, and remove it if it does (storing the URI for later)
|
||||
var frChanged bool
|
||||
var frURI string
|
||||
fr := >smodel.FollowRequest{}
|
||||
if err := p.db.GetWhere([]db.Where{
|
||||
{Key: "account_id", Value: authed.Account.ID},
|
||||
{Key: "target_account_id", Value: targetAccountID},
|
||||
}, fr); err == nil {
|
||||
frURI = fr.URI
|
||||
if err := p.db.DeleteByID(fr.ID, fr); err != nil {
|
||||
return nil, NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow request from db: %s", err))
|
||||
}
|
||||
frChanged = true
|
||||
}
|
||||
|
||||
// now do the same thing for any existing follow
|
||||
var fChanged bool
|
||||
var fURI string
|
||||
f := >smodel.Follow{}
|
||||
if err := p.db.GetWhere([]db.Where{
|
||||
{Key: "account_id", Value: authed.Account.ID},
|
||||
{Key: "target_account_id", Value: targetAccountID},
|
||||
}, f); err == nil {
|
||||
fURI = f.URI
|
||||
if err := p.db.DeleteByID(f.ID, f); err != nil {
|
||||
return nil, NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow from db: %s", err))
|
||||
}
|
||||
fChanged = true
|
||||
}
|
||||
|
||||
// follow request status changed so send the UNDO activity to the channel for async processing
|
||||
if frChanged {
|
||||
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
||||
APObjectType: gtsmodel.ActivityStreamsFollow,
|
||||
APActivityType: gtsmodel.ActivityStreamsUndo,
|
||||
GTSModel: >smodel.Follow{
|
||||
AccountID: authed.Account.ID,
|
||||
TargetAccountID: targetAccountID,
|
||||
URI: frURI,
|
||||
},
|
||||
OriginAccount: authed.Account,
|
||||
TargetAccount: targetAcct,
|
||||
}
|
||||
}
|
||||
|
||||
// follow status changed so send the UNDO activity to the channel for async processing
|
||||
if fChanged {
|
||||
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
||||
APObjectType: gtsmodel.ActivityStreamsFollow,
|
||||
APActivityType: gtsmodel.ActivityStreamsUndo,
|
||||
GTSModel: >smodel.Follow{
|
||||
AccountID: authed.Account.ID,
|
||||
TargetAccountID: targetAccountID,
|
||||
URI: fURI,
|
||||
},
|
||||
OriginAccount: authed.Account,
|
||||
TargetAccount: targetAcct,
|
||||
}
|
||||
}
|
||||
|
||||
// return whatever relationship results from all this
|
||||
return p.AccountRelationshipGet(authed, targetAccountID)
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/go-fed/activity/streams"
|
||||
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
|
||||
requestingAccount := >smodel.Account{}
|
||||
|
||||
err = p.db.GetWhere("uri", requestingAccountURI.String(), requestingAccount)
|
||||
err = p.db.GetWhere([]db.Where{{Key: "uri", Value: requestingAccountURI.String()}}, requestingAccount)
|
||||
if err == nil {
|
||||
// we do have it yay, return it
|
||||
return requestingAccount, nil
|
||||
|
@ -122,6 +123,89 @@ func (p *processor) GetFediUser(requestedUsername string, request *http.Request)
|
|||
return data, nil
|
||||
}
|
||||
|
||||
func (p *processor) GetFediFollowers(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) {
|
||||
// get the account the request is referring to
|
||||
requestedAccount := >smodel.Account{}
|
||||
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
|
||||
return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
|
||||
}
|
||||
|
||||
// authenticate the request
|
||||
requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
|
||||
if err != nil {
|
||||
return nil, NewErrorNotAuthorized(err)
|
||||
}
|
||||
|
||||
blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID)
|
||||
if err != nil {
|
||||
return nil, NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if blocked {
|
||||
return nil, NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
|
||||
}
|
||||
|
||||
requestedAccountURI, err := url.Parse(requestedAccount.URI)
|
||||
if err != nil {
|
||||
return nil, NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err))
|
||||
}
|
||||
|
||||
requestedFollowers, err := p.federator.FederatingDB().Followers(context.Background(), requestedAccountURI)
|
||||
if err != nil {
|
||||
return nil, NewErrorInternalError(fmt.Errorf("error fetching followers for uri %s: %s", requestedAccountURI.String(), err))
|
||||
}
|
||||
|
||||
data, err := streams.Serialize(requestedFollowers)
|
||||
if err != nil {
|
||||
return nil, NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, ErrorWithCode) {
|
||||
// get the account the request is referring to
|
||||
requestedAccount := >smodel.Account{}
|
||||
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
|
||||
return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
|
||||
}
|
||||
|
||||
// authenticate the request
|
||||
requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
|
||||
if err != nil {
|
||||
return nil, NewErrorNotAuthorized(err)
|
||||
}
|
||||
|
||||
blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID)
|
||||
if err != nil {
|
||||
return nil, NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if blocked {
|
||||
return nil, NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
|
||||
}
|
||||
|
||||
s := >smodel.Status{}
|
||||
if err := p.db.GetWhere([]db.Where{
|
||||
{Key: "id", Value: requestedStatusID},
|
||||
{Key: "account_id", Value: requestedAccount.ID},
|
||||
}, s); err != nil {
|
||||
return nil, NewErrorNotFound(fmt.Errorf("database error getting status with id %s and account id %s: %s", requestedStatusID, requestedAccount.ID, err))
|
||||
}
|
||||
|
||||
asStatus, err := p.tc.StatusToAS(s)
|
||||
if err != nil {
|
||||
return nil, NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
data, err := streams.Serialize(asStatus)
|
||||
if err != nil {
|
||||
return nil, NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (p *processor) GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, ErrorWithCode) {
|
||||
// get the account the request is referring to
|
||||
requestedAccount := >smodel.Account{}
|
||||
|
|
|
@ -19,15 +19,22 @@
|
|||
package message
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/go-fed/activity/streams"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error {
|
||||
switch clientMsg.APActivityType {
|
||||
case gtsmodel.ActivityStreamsCreate:
|
||||
// CREATE
|
||||
switch clientMsg.APObjectType {
|
||||
case gtsmodel.ActivityStreamsNote:
|
||||
// CREATE NOTE
|
||||
status, ok := clientMsg.GTSModel.(*gtsmodel.Status)
|
||||
if !ok {
|
||||
return errors.New("note was not parseable as *gtsmodel.Status")
|
||||
|
@ -41,33 +48,169 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error
|
|||
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")
|
||||
}
|
||||
return fmt.Errorf("message type unprocessable: %+v", clientMsg)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (p *processor) federateStatus(status *gtsmodel.Status) error {
|
||||
// // derive the sending account -- it might be attached to the status already
|
||||
// sendingAcct := >smodel.Account{}
|
||||
// if status.GTSAccount != nil {
|
||||
// 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
|
||||
// }
|
||||
// }
|
||||
asStatus, err := p.tc.StatusToAS(status)
|
||||
if err != nil {
|
||||
return fmt.Errorf("federateStatus: error converting status to as format: %s", err)
|
||||
}
|
||||
|
||||
// outboxURI, err := url.Parse(sendingAcct.OutboxURI)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
outboxIRI, err := url.Parse(status.GTSAccount.OutboxURI)
|
||||
if err != nil {
|
||||
return fmt.Errorf("federateStatus: error parsing outboxURI %s: %s", status.GTSAccount.OutboxURI, err)
|
||||
}
|
||||
|
||||
// // convert the status to AS format Note
|
||||
// note, err := p.tc.StatusToAS(status)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// _, err = p.federator.FederatingActor().Send(context.Background(), outboxURI, note)
|
||||
return nil
|
||||
_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, asStatus)
|
||||
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
|
||||
if originAccount.Domain == "" && targetAccount.Domain == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
asFollow, err := p.tc.FollowToAS(follow, originAccount, targetAccount)
|
||||
if err != nil {
|
||||
return fmt.Errorf("federateFollow: error converting follow to as format: %s", err)
|
||||
}
|
||||
|
||||
outboxIRI, err := url.Parse(originAccount.OutboxURI)
|
||||
if err != nil {
|
||||
return fmt.Errorf("federateFollow: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
|
||||
}
|
||||
|
||||
_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, asFollow)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *processor) federateUnfollow(follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
|
||||
// if both accounts are local there's nothing to do here
|
||||
if originAccount.Domain == "" && targetAccount.Domain == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// recreate the follow
|
||||
asFollow, err := p.tc.FollowToAS(follow, originAccount, targetAccount)
|
||||
if err != nil {
|
||||
return fmt.Errorf("federateUnfollow: error converting follow to as format: %s", err)
|
||||
}
|
||||
|
||||
targetAccountURI, err := url.Parse(targetAccount.URI)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing uri %s: %s", targetAccount.URI, err)
|
||||
}
|
||||
|
||||
// create an Undo and set the appropriate actor on it
|
||||
undo := streams.NewActivityStreamsUndo()
|
||||
undo.SetActivityStreamsActor(asFollow.GetActivityStreamsActor())
|
||||
|
||||
// Set the recreated follow as the 'object' property.
|
||||
undoObject := streams.NewActivityStreamsObjectProperty()
|
||||
undoObject.AppendActivityStreamsFollow(asFollow)
|
||||
undo.SetActivityStreamsObject(undoObject)
|
||||
|
||||
// Set the To of the undo as the target of the recreated follow
|
||||
undoTo := streams.NewActivityStreamsToProperty()
|
||||
undoTo.AppendIRI(targetAccountURI)
|
||||
undo.SetActivityStreamsTo(undoTo)
|
||||
|
||||
outboxIRI, err := url.Parse(originAccount.OutboxURI)
|
||||
if err != nil {
|
||||
return fmt.Errorf("federateUnfollow: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
|
||||
}
|
||||
|
||||
// send off the Undo
|
||||
_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, undo)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *processor) federateAcceptFollowRequest(follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
|
||||
// if both accounts are local there's nothing to do here
|
||||
if originAccount.Domain == "" && targetAccount.Domain == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// recreate the AS follow
|
||||
asFollow, err := p.tc.FollowToAS(follow, originAccount, targetAccount)
|
||||
if err != nil {
|
||||
return fmt.Errorf("federateUnfollow: error converting follow to as format: %s", err)
|
||||
}
|
||||
|
||||
acceptingAccountURI, err := url.Parse(targetAccount.URI)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing uri %s: %s", targetAccount.URI, err)
|
||||
}
|
||||
|
||||
requestingAccountURI, err := url.Parse(originAccount.URI)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing uri %s: %s", targetAccount.URI, err)
|
||||
}
|
||||
|
||||
// create an Accept
|
||||
accept := streams.NewActivityStreamsAccept()
|
||||
|
||||
// set the accepting actor on it
|
||||
acceptActorProp := streams.NewActivityStreamsActorProperty()
|
||||
acceptActorProp.AppendIRI(acceptingAccountURI)
|
||||
accept.SetActivityStreamsActor(acceptActorProp)
|
||||
|
||||
// Set the recreated follow as the 'object' property.
|
||||
acceptObject := streams.NewActivityStreamsObjectProperty()
|
||||
acceptObject.AppendActivityStreamsFollow(asFollow)
|
||||
accept.SetActivityStreamsObject(acceptObject)
|
||||
|
||||
// Set the To of the accept as the originator of the follow
|
||||
acceptTo := streams.NewActivityStreamsToProperty()
|
||||
acceptTo.AppendIRI(requestingAccountURI)
|
||||
accept.SetActivityStreamsTo(acceptTo)
|
||||
|
||||
outboxIRI, err := url.Parse(targetAccount.OutboxURI)
|
||||
if err != nil {
|
||||
return fmt.Errorf("federateAcceptFollowRequest: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
|
||||
}
|
||||
|
||||
// send off the accept using the accepter's outbox
|
||||
_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, accept)
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -23,3 +23,7 @@ import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
|||
func (p *processor) notifyStatus(status *gtsmodel.Status) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *processor) notifyFollow(follow *gtsmodel.Follow) error {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -38,9 +38,12 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
|
|||
|
||||
l.Debug("entering function PROCESS FROM FEDERATOR")
|
||||
|
||||
switch federatorMsg.APActivityType {
|
||||
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")
|
||||
|
@ -57,6 +60,39 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
|
|||
if err := p.notifyStatus(incomingStatus); err != nil {
|
||||
return err
|
||||
}
|
||||
case gtsmodel.ActivityStreamsProfile:
|
||||
// CREATE 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)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -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
|
||||
maybeAttachment := >smodel.MediaAttachment{}
|
||||
err := p.db.GetWhere("remote_url", a.RemoteURL, maybeAttachment)
|
||||
err := p.db.GetWhere([]db.Where{{Key: "remote_url", Value: a.RemoteURL}}, maybeAttachment)
|
||||
if err == nil {
|
||||
// we already have it in the db, dereferenced, no need to do it again
|
||||
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
|
||||
|
||||
targetAccount := >smodel.Account{}
|
||||
if err := p.db.GetWhere("uri", uri.String(), targetAccount); err != nil {
|
||||
if err := p.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, targetAccount); err != nil {
|
||||
// proper error
|
||||
if _, ok := err.(db.ErrNoEntries); !ok {
|
||||
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
|
||||
}
|
||||
|
||||
func (p *processor) dereferenceAccountFields(account *gtsmodel.Account, requestingUsername string) error {
|
||||
l := p.log.WithFields(logrus.Fields{
|
||||
"func": "dereferenceAccountFields",
|
||||
"requestingUsername": requestingUsername,
|
||||
})
|
||||
|
||||
t, err := p.federator.GetTransportForUser(requestingUsername)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting transport for user: %s", err)
|
||||
}
|
||||
|
||||
// fetch the header and avatar
|
||||
if err := p.fetchHeaderAndAviForAccount(account, t); err != nil {
|
||||
// if this doesn't work, just skip it -- we can do it later
|
||||
l.Debugf("error fetching header/avi for account: %s", err)
|
||||
}
|
||||
|
||||
if err := p.db.UpdateByID(account.ID, account); err != nil {
|
||||
return fmt.Errorf("error updating account in database: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -48,11 +48,28 @@ func (p *processor) FollowRequestsGet(auth *oauth.Auth) ([]apimodel.Account, Err
|
|||
return accts, nil
|
||||
}
|
||||
|
||||
func (p *processor) FollowRequestAccept(auth *oauth.Auth, accountID string) ErrorWithCode {
|
||||
if err := p.db.AcceptFollowRequest(accountID, auth.Account.ID); err != nil {
|
||||
return NewErrorNotFound(err)
|
||||
func (p *processor) FollowRequestAccept(auth *oauth.Auth, accountID string) (*apimodel.Relationship, ErrorWithCode) {
|
||||
follow, err := p.db.AcceptFollowRequest(accountID, auth.Account.ID)
|
||||
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 {
|
||||
|
|
|
@ -22,12 +22,13 @@ import (
|
|||
"fmt"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
func (p *processor) InstanceGet(domain string) (*apimodel.Instance, ErrorWithCode) {
|
||||
i := >smodel.Instance{}
|
||||
if err := p.db.GetWhere("domain", domain, i); err != nil {
|
||||
if err := p.db.GetWhere([]db.Where{{Key: "domain", Value: domain}}, i); err != nil {
|
||||
return nil, NewErrorInternalError(fmt.Errorf("db error fetching instance %s: %s", p.config.Host, err))
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
// the account given in authed.
|
||||
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)
|
||||
// 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(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(auth *oauth.Auth) ([]apimodel.Account, ErrorWithCode)
|
||||
// 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(domain string) (*apimodel.Instance, ErrorWithCode)
|
||||
|
@ -125,6 +133,14 @@ type Processor interface {
|
|||
// before returning a JSON serializable interface to the caller.
|
||||
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(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, ErrorWithCode)
|
||||
|
||||
|
@ -200,15 +216,11 @@ func (p *processor) Start() error {
|
|||
DistLoop:
|
||||
for {
|
||||
select {
|
||||
// case clientMsg := <-p.toClientAPI:
|
||||
// p.log.Infof("received message TO client API: %+v", clientMsg)
|
||||
case clientMsg := <-p.fromClientAPI:
|
||||
p.log.Infof("received message FROM client API: %+v", clientMsg)
|
||||
if err := p.processFromClientAPI(clientMsg); err != nil {
|
||||
p.log.Error(err)
|
||||
}
|
||||
// case federatorMsg := <-p.toFederator:
|
||||
// p.log.Infof("received message TO federator: %+v", federatorMsg)
|
||||
case federatorMsg := <-p.fromFederator:
|
||||
p.log.Infof("received message FROM federator: %+v", federatorMsg)
|
||||
if err := p.processFromFederator(federatorMsg); err != nil {
|
||||
|
|
|
@ -29,6 +29,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
||||
"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 {
|
||||
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
|
||||
status.GTSMentions = gtsMenchies
|
||||
|
@ -280,7 +281,7 @@ func (p *processor) updateAccountAvatar(avatar *multipart.FileHeader, accountID
|
|||
}
|
||||
|
||||
// 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 {
|
||||
return nil, fmt.Errorf("error processing avatar: %s", err)
|
||||
}
|
||||
|
@ -313,10 +314,42 @@ func (p *processor) updateAccountHeader(header *multipart.FileHeader, accountID
|
|||
}
|
||||
|
||||
// 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 {
|
||||
return nil, fmt.Errorf("error processing header: %s", err)
|
||||
}
|
||||
|
||||
return headerInfo, f.Close()
|
||||
}
|
||||
|
||||
// fetchHeaderAndAviForAccount fetches the header and avatar for a remote account, using a transport
|
||||
// on behalf of requestingUsername.
|
||||
//
|
||||
// targetAccount's AvatarMediaAttachmentID and HeaderMediaAttachmentID will be updated as necessary.
|
||||
//
|
||||
// SIDE EFFECTS: remote header and avatar will be stored in local storage, and the database will be updated
|
||||
// to reflect the creation of these new attachments.
|
||||
func (p *processor) fetchHeaderAndAviForAccount(targetAccount *gtsmodel.Account, t transport.Transport) error {
|
||||
if targetAccount.AvatarRemoteURL != "" && targetAccount.AvatarMediaAttachmentID == "" {
|
||||
a, err := p.mediaHandler.ProcessRemoteHeaderOrAvatar(t, >smodel.MediaAttachment{
|
||||
RemoteURL: targetAccount.AvatarRemoteURL,
|
||||
Avatar: true,
|
||||
}, targetAccount.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error processing avatar for user: %s", err)
|
||||
}
|
||||
targetAccount.AvatarMediaAttachmentID = a.ID
|
||||
}
|
||||
|
||||
if targetAccount.HeaderRemoteURL != "" && targetAccount.HeaderMediaAttachmentID == "" {
|
||||
a, err := p.mediaHandler.ProcessRemoteHeaderOrAvatar(t, >smodel.MediaAttachment{
|
||||
RemoteURL: targetAccount.HeaderRemoteURL,
|
||||
Header: true,
|
||||
}, targetAccount.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error processing header for user: %s", err)
|
||||
}
|
||||
targetAccount.HeaderMediaAttachmentID = a.ID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -106,17 +106,17 @@ func (pts *tokenStore) Create(ctx context.Context, info oauth2.TokenInfo) error
|
|||
|
||||
// RemoveByCode deletes a token from the DB based on the Code field
|
||||
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
|
||||
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
|
||||
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
|
||||
|
@ -127,7 +127,7 @@ func (pts *tokenStore) GetByCode(ctx context.Context, code string) (oauth2.Token
|
|||
pgt := &Token{
|
||||
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 TokenToOauthToken(pgt), nil
|
||||
|
@ -141,7 +141,7 @@ func (pts *tokenStore) GetByAccess(ctx context.Context, access string) (oauth2.T
|
|||
pgt := &Token{
|
||||
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 TokenToOauthToken(pgt), nil
|
||||
|
@ -155,7 +155,7 @@ func (pts *tokenStore) GetByRefresh(ctx context.Context, refresh string) (oauth2
|
|||
pgt := &Token{
|
||||
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 TokenToOauthToken(pgt), nil
|
||||
|
|
|
@ -39,6 +39,7 @@ type controller struct {
|
|||
clock pub.Clock
|
||||
client pub.HttpClient
|
||||
appAgent string
|
||||
log *logrus.Logger
|
||||
}
|
||||
|
||||
// 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,
|
||||
client: client,
|
||||
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,
|
||||
getSigner: getSigner,
|
||||
getSignerMu: &sync.Mutex{},
|
||||
log: c.log,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
|
||||
"github.com/go-fed/activity/pub"
|
||||
"github.com/go-fed/httpsig"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Transport wraps the pub.Transport interface with some additional
|
||||
|
@ -31,6 +32,7 @@ type transport struct {
|
|||
sigTransport *pub.HttpSigTransport
|
||||
getSigner httpsig.Signer
|
||||
getSignerMu *sync.Mutex
|
||||
log *logrus.Logger
|
||||
}
|
||||
|
||||
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 {
|
||||
l := t.log.WithField("func", "Deliver")
|
||||
l.Debugf("performing POST to %s", to.String())
|
||||
return t.sigTransport.Deliver(c, b, to)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -37,7 +37,7 @@ func (c *converter) ASRepresentationToAccount(accountable Accountable) (*gtsmode
|
|||
uri := uriProp.GetIRI()
|
||||
|
||||
acct := >smodel.Account{}
|
||||
err := c.db.GetWhere("uri", uri.String(), acct)
|
||||
err := c.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, acct)
|
||||
if err == nil {
|
||||
// we already know this account so we can skip generating it
|
||||
return acct, nil
|
||||
|
@ -90,7 +90,7 @@ func (c *converter) ASRepresentationToAccount(accountable Accountable) (*gtsmode
|
|||
}
|
||||
|
||||
// check for bot and actor type
|
||||
switch gtsmodel.ActivityStreamsActor(accountable.GetTypeName()) {
|
||||
switch accountable.GetTypeName() {
|
||||
case gtsmodel.ActivityStreamsPerson, gtsmodel.ActivityStreamsGroup, gtsmodel.ActivityStreamsOrganization:
|
||||
// people, groups, and organizations aren't bots
|
||||
acct.Bot = false
|
||||
|
@ -101,7 +101,7 @@ func (c *converter) ASRepresentationToAccount(accountable Accountable) (*gtsmode
|
|||
// we don't know what this is!
|
||||
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
|
||||
|
||||
|
@ -220,7 +220,7 @@ func (c *converter) ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, e
|
|||
status.APStatusOwnerURI = attributedTo.String()
|
||||
|
||||
statusOwner := >smodel.Account{}
|
||||
if err := c.db.GetWhere("uri", attributedTo.String(), statusOwner); err != nil {
|
||||
if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: attributedTo.String()}}, statusOwner); err != nil {
|
||||
return nil, fmt.Errorf("couldn't get status owner from db: %s", err)
|
||||
}
|
||||
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
|
||||
inReplyToStatus := >smodel.Status{}
|
||||
if err := c.db.GetWhere("uri", inReplyToURI.String(), inReplyToStatus); err == nil {
|
||||
if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: inReplyToURI.String()}}, inReplyToStatus); err == nil {
|
||||
// we have the status in our database already
|
||||
// so we can set these fields here and then...
|
||||
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
|
||||
// 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
|
||||
}
|
||||
|
||||
|
@ -301,7 +304,7 @@ func (c *converter) ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, e
|
|||
// we might be able to extract this from the contentMap field
|
||||
|
||||
// ActivityStreamsType
|
||||
status.ActivityStreamsType = gtsmodel.ActivityStreamsObject(statusable.GetTypeName())
|
||||
status.ActivityStreamsType = statusable.GetTypeName()
|
||||
|
||||
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")
|
||||
}
|
||||
originAccount := >smodel.Account{}
|
||||
if err := c.db.GetWhere("uri", origin.String(), originAccount); err != nil {
|
||||
if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: origin.String()}}, originAccount); err != nil {
|
||||
return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err)
|
||||
}
|
||||
|
||||
|
@ -328,7 +331,7 @@ func (c *converter) ASFollowToFollowRequest(followable Followable) (*gtsmodel.Fo
|
|||
return nil, errors.New("error extracting object property from follow")
|
||||
}
|
||||
targetAccount := >smodel.Account{}
|
||||
if err := c.db.GetWhere("uri", target.String(), targetAccount); err != nil {
|
||||
if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: target.String()}}, targetAccount); err != nil {
|
||||
return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err)
|
||||
}
|
||||
|
||||
|
@ -341,6 +344,40 @@ func (c *converter) ASFollowToFollowRequest(followable Followable) (*gtsmodel.Fo
|
|||
return followRequest, nil
|
||||
}
|
||||
|
||||
func (c *converter) ASFollowToFollow(followable Followable) (*gtsmodel.Follow, error) {
|
||||
idProp := followable.GetJSONLDId()
|
||||
if idProp == nil || !idProp.IsIRI() {
|
||||
return nil, errors.New("no id property set on follow, or was not an iri")
|
||||
}
|
||||
uri := idProp.GetIRI().String()
|
||||
|
||||
origin, err := extractActor(followable)
|
||||
if err != nil {
|
||||
return nil, errors.New("error extracting actor property from follow")
|
||||
}
|
||||
originAccount := >smodel.Account{}
|
||||
if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: origin.String()}}, originAccount); err != nil {
|
||||
return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err)
|
||||
}
|
||||
|
||||
target, err := extractObject(followable)
|
||||
if err != nil {
|
||||
return nil, errors.New("error extracting object property from follow")
|
||||
}
|
||||
targetAccount := >smodel.Account{}
|
||||
if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: target.String()}}, targetAccount); err != nil {
|
||||
return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err)
|
||||
}
|
||||
|
||||
follow := >smodel.Follow{
|
||||
URI: uri,
|
||||
AccountID: originAccount.ID,
|
||||
TargetAccountID: targetAccount.ID,
|
||||
}
|
||||
|
||||
return follow, nil
|
||||
}
|
||||
|
||||
func isPublic(tos []*url.URL) bool {
|
||||
for _, entry := range tos {
|
||||
if strings.EqualFold(entry.String(), "https://www.w3.org/ns/activitystreams#Public") {
|
||||
|
|
|
@ -26,6 +26,10 @@ import (
|
|||
"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,
|
||||
// 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(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
|
||||
*/
|
||||
|
@ -94,6 +101,8 @@ type TypeConverter interface {
|
|||
ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, error)
|
||||
// ASFollowToFollowRequest converts a remote activitystreams `follow` representation into gts model follow request.
|
||||
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
|
||||
|
@ -104,6 +113,15 @@ type TypeConverter interface {
|
|||
|
||||
// StatusToAS converts a gts model status into an activity streams note, suitable for federation
|
||||
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 {
|
||||
|
|
|
@ -21,10 +21,12 @@ package typeutils
|
|||
import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/go-fed/activity/streams"
|
||||
"github.com/go-fed/activity/streams/vocab"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"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) {
|
||||
return nil, nil
|
||||
// ensure prerequisites here before we get stuck in
|
||||
|
||||
// check if author account is already attached to status and attach it if not
|
||||
// if we can't retrieve this, bail here already because we can't attribute the status to anyone
|
||||
if s.GTSAccount == nil {
|
||||
a := >smodel.Account{}
|
||||
if err := c.db.GetByID(s.AccountID, a); err != nil {
|
||||
return nil, fmt.Errorf("StatusToAS: error retrieving author account from db: %s", err)
|
||||
}
|
||||
s.GTSAccount = a
|
||||
}
|
||||
|
||||
// create the Note!
|
||||
status := streams.NewActivityStreamsNote()
|
||||
|
||||
// id
|
||||
statusURI, err := url.Parse(s.URI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.URI, err)
|
||||
}
|
||||
statusIDProp := streams.NewJSONLDIdProperty()
|
||||
statusIDProp.SetIRI(statusURI)
|
||||
status.SetJSONLDId(statusIDProp)
|
||||
|
||||
// type
|
||||
// will be set automatically by go-fed
|
||||
|
||||
// summary aka cw
|
||||
statusSummaryProp := streams.NewActivityStreamsSummaryProperty()
|
||||
statusSummaryProp.AppendXMLSchemaString(s.ContentWarning)
|
||||
status.SetActivityStreamsSummary(statusSummaryProp)
|
||||
|
||||
// inReplyTo
|
||||
if s.InReplyToID != "" {
|
||||
// fetch the replied status if we don't have it on hand already
|
||||
if s.GTSReplyToStatus == nil {
|
||||
rs := >smodel.Status{}
|
||||
if err := c.db.GetByID(s.InReplyToID, rs); err != nil {
|
||||
return nil, fmt.Errorf("StatusToAS: error retrieving replied-to status from db: %s", err)
|
||||
}
|
||||
s.GTSReplyToStatus = rs
|
||||
}
|
||||
rURI, err := url.Parse(s.GTSReplyToStatus.URI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.GTSReplyToStatus.URI, err)
|
||||
}
|
||||
|
||||
inReplyToProp := streams.NewActivityStreamsInReplyToProperty()
|
||||
inReplyToProp.AppendIRI(rURI)
|
||||
status.SetActivityStreamsInReplyTo(inReplyToProp)
|
||||
}
|
||||
|
||||
// published
|
||||
publishedProp := streams.NewActivityStreamsPublishedProperty()
|
||||
publishedProp.Set(s.CreatedAt)
|
||||
status.SetActivityStreamsPublished(publishedProp)
|
||||
|
||||
// url
|
||||
if s.URL != "" {
|
||||
sURL, err := url.Parse(s.URL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.URL, err)
|
||||
}
|
||||
|
||||
urlProp := streams.NewActivityStreamsUrlProperty()
|
||||
urlProp.AppendIRI(sURL)
|
||||
status.SetActivityStreamsUrl(urlProp)
|
||||
}
|
||||
|
||||
// attributedTo
|
||||
authorAccountURI, err := url.Parse(s.GTSAccount.URI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.GTSAccount.URI, err)
|
||||
}
|
||||
attributedToProp := streams.NewActivityStreamsAttributedToProperty()
|
||||
attributedToProp.AppendIRI(authorAccountURI)
|
||||
status.SetActivityStreamsAttributedTo(attributedToProp)
|
||||
|
||||
// tags
|
||||
tagProp := streams.NewActivityStreamsTagProperty()
|
||||
|
||||
// tag -- mentions
|
||||
for _, m := range s.GTSMentions {
|
||||
asMention, err := c.MentionToAS(m)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("StatusToAS: error converting mention to AS mention: %s", err)
|
||||
}
|
||||
tagProp.AppendActivityStreamsMention(asMention)
|
||||
}
|
||||
|
||||
// tag -- emojis
|
||||
// TODO
|
||||
|
||||
// tag -- hashtags
|
||||
// TODO
|
||||
|
||||
status.SetActivityStreamsTag(tagProp)
|
||||
|
||||
// parse out some URIs we need here
|
||||
authorFollowersURI, err := url.Parse(s.GTSAccount.FollowersURI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.GTSAccount.FollowersURI, err)
|
||||
}
|
||||
|
||||
publicURI, err := url.Parse(asPublicURI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", asPublicURI, err)
|
||||
}
|
||||
|
||||
// to and cc
|
||||
toProp := streams.NewActivityStreamsToProperty()
|
||||
ccProp := streams.NewActivityStreamsCcProperty()
|
||||
switch s.Visibility {
|
||||
case gtsmodel.VisibilityDirect:
|
||||
// if DIRECT, then only mentioned users should be added to TO, and nothing to CC
|
||||
for _, m := range s.GTSMentions {
|
||||
iri, err := url.Parse(m.GTSAccount.URI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.GTSAccount.URI, err)
|
||||
}
|
||||
toProp.AppendIRI(iri)
|
||||
}
|
||||
case gtsmodel.VisibilityMutualsOnly:
|
||||
// TODO
|
||||
case gtsmodel.VisibilityFollowersOnly:
|
||||
// if FOLLOWERS ONLY then we want to add followers to TO, and mentions to CC
|
||||
toProp.AppendIRI(authorFollowersURI)
|
||||
for _, m := range s.GTSMentions {
|
||||
iri, err := url.Parse(m.GTSAccount.URI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.GTSAccount.URI, err)
|
||||
}
|
||||
ccProp.AppendIRI(iri)
|
||||
}
|
||||
case gtsmodel.VisibilityUnlocked:
|
||||
// if UNLOCKED, we want to add followers to TO, and public and mentions to CC
|
||||
toProp.AppendIRI(authorFollowersURI)
|
||||
ccProp.AppendIRI(publicURI)
|
||||
for _, m := range s.GTSMentions {
|
||||
iri, err := url.Parse(m.GTSAccount.URI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.GTSAccount.URI, err)
|
||||
}
|
||||
ccProp.AppendIRI(iri)
|
||||
}
|
||||
case gtsmodel.VisibilityPublic:
|
||||
// if PUBLIC, we want to add public to TO, and followers and mentions to CC
|
||||
toProp.AppendIRI(publicURI)
|
||||
ccProp.AppendIRI(authorFollowersURI)
|
||||
for _, m := range s.GTSMentions {
|
||||
iri, err := url.Parse(m.GTSAccount.URI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.GTSAccount.URI, err)
|
||||
}
|
||||
ccProp.AppendIRI(iri)
|
||||
}
|
||||
}
|
||||
status.SetActivityStreamsTo(toProp)
|
||||
status.SetActivityStreamsCc(ccProp)
|
||||
|
||||
// conversation
|
||||
// TODO
|
||||
|
||||
// content -- the actual post itself
|
||||
contentProp := streams.NewActivityStreamsContentProperty()
|
||||
contentProp.AppendXMLSchemaString(s.Content)
|
||||
status.SetActivityStreamsContent(contentProp)
|
||||
|
||||
// attachment
|
||||
attachmentProp := streams.NewActivityStreamsAttachmentProperty()
|
||||
for _, a := range s.GTSMediaAttachments {
|
||||
doc, err := c.AttachmentToAS(a)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("StatusToAS: error converting attachment: %s", err)
|
||||
}
|
||||
attachmentProp.AppendActivityStreamsDocument(doc)
|
||||
}
|
||||
status.SetActivityStreamsAttachment(attachmentProp)
|
||||
|
||||
// replies
|
||||
// TODO
|
||||
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func (c *converter) FollowToAS(f *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (vocab.ActivityStreamsFollow, error) {
|
||||
// parse out the various URIs we need for this
|
||||
// origin account (who's doing the follow)
|
||||
originAccountURI, err := url.Parse(originAccount.URI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("followtoasfollow: error parsing origin account uri: %s", err)
|
||||
}
|
||||
originActor := streams.NewActivityStreamsActorProperty()
|
||||
originActor.AppendIRI(originAccountURI)
|
||||
|
||||
// target account (who's being followed)
|
||||
targetAccountURI, err := url.Parse(targetAccount.URI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("followtoasfollow: error parsing target account uri: %s", err)
|
||||
}
|
||||
|
||||
// uri of the follow activity itself
|
||||
followURI, err := url.Parse(f.URI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("followtoasfollow: error parsing follow uri: %s", err)
|
||||
}
|
||||
|
||||
// start preparing the follow activity
|
||||
follow := streams.NewActivityStreamsFollow()
|
||||
|
||||
// set the actor
|
||||
follow.SetActivityStreamsActor(originActor)
|
||||
|
||||
// set the id
|
||||
followIDProp := streams.NewJSONLDIdProperty()
|
||||
followIDProp.SetIRI(followURI)
|
||||
follow.SetJSONLDId(followIDProp)
|
||||
|
||||
// set the object
|
||||
followObjectProp := streams.NewActivityStreamsObjectProperty()
|
||||
followObjectProp.AppendIRI(targetAccountURI)
|
||||
follow.SetActivityStreamsObject(followObjectProp)
|
||||
|
||||
// set the To property
|
||||
followToProp := streams.NewActivityStreamsToProperty()
|
||||
followToProp.AppendIRI(targetAccountURI)
|
||||
follow.SetActivityStreamsTo(followToProp)
|
||||
|
||||
return follow, nil
|
||||
}
|
||||
|
||||
func (c *converter) MentionToAS(m *gtsmodel.Mention) (vocab.ActivityStreamsMention, error) {
|
||||
if m.GTSAccount == nil {
|
||||
a := >smodel.Account{}
|
||||
if err := c.db.GetWhere([]db.Where{{Key: "target_account_id", Value: m.TargetAccountID}}, a); err != nil {
|
||||
return nil, fmt.Errorf("MentionToAS: error getting target account from db: %s", err)
|
||||
}
|
||||
m.GTSAccount = a
|
||||
}
|
||||
|
||||
// create the mention
|
||||
mention := streams.NewActivityStreamsMention()
|
||||
|
||||
// href -- this should be the URI of the mentioned user
|
||||
hrefProp := streams.NewActivityStreamsHrefProperty()
|
||||
hrefURI, err := url.Parse(m.GTSAccount.URI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("MentionToAS: error parsing uri %s: %s", m.GTSAccount.URI, err)
|
||||
}
|
||||
hrefProp.SetIRI(hrefURI)
|
||||
mention.SetActivityStreamsHref(hrefProp)
|
||||
|
||||
// name -- this should be the namestring of the mentioned user, something like @whatever@example.org
|
||||
var domain string
|
||||
if m.GTSAccount.Domain == "" {
|
||||
domain = c.config.Host
|
||||
} else {
|
||||
domain = m.GTSAccount.Domain
|
||||
}
|
||||
username := m.GTSAccount.Username
|
||||
nameString := fmt.Sprintf("@%s@%s", username, domain)
|
||||
nameProp := streams.NewActivityStreamsNameProperty()
|
||||
nameProp.AppendXMLSchemaString(nameString)
|
||||
mention.SetActivityStreamsName(nameProp)
|
||||
|
||||
return mention, nil
|
||||
}
|
||||
|
||||
func (c *converter) AttachmentToAS(a *gtsmodel.MediaAttachment) (vocab.ActivityStreamsDocument, error) {
|
||||
// type -- Document
|
||||
doc := streams.NewActivityStreamsDocument()
|
||||
|
||||
// mediaType aka mime content type
|
||||
mediaTypeProp := streams.NewActivityStreamsMediaTypeProperty()
|
||||
mediaTypeProp.Set(a.File.ContentType)
|
||||
doc.SetActivityStreamsMediaType(mediaTypeProp)
|
||||
|
||||
// url -- for the original image not the thumbnail
|
||||
urlProp := streams.NewActivityStreamsUrlProperty()
|
||||
imageURL, err := url.Parse(a.URL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("AttachmentToAS: error parsing uri %s: %s", a.URL, err)
|
||||
}
|
||||
urlProp.AppendIRI(imageURL)
|
||||
doc.SetActivityStreamsUrl(urlProp)
|
||||
|
||||
// name -- aka image description
|
||||
nameProp := streams.NewActivityStreamsNameProperty()
|
||||
nameProp.AppendXMLSchemaString(a.Description)
|
||||
doc.SetActivityStreamsName(nameProp)
|
||||
|
||||
// blurhash
|
||||
blurProp := streams.NewTootBlurhashProperty()
|
||||
blurProp.Set(a.Blurhash)
|
||||
doc.SetTootBlurhash(blurProp)
|
||||
|
||||
// focalpoint
|
||||
// TODO
|
||||
|
||||
return doc, nil
|
||||
}
|
||||
|
|
|
@ -572,3 +572,21 @@ func (c *converter) InstanceToMasto(i *gtsmodel.Instance) (*model.Instance, erro
|
|||
|
||||
return mi, nil
|
||||
}
|
||||
|
||||
func (c *converter) RelationshipToMasto(r *gtsmodel.Relationship) (*model.Relationship, error) {
|
||||
return &model.Relationship{
|
||||
ID: r.ID,
|
||||
Following: r.Following,
|
||||
ShowingReblogs: r.ShowingReblogs,
|
||||
Notifying: r.Notifying,
|
||||
FollowedBy: r.FollowedBy,
|
||||
Blocking: r.Blocking,
|
||||
BlockedBy: r.BlockedBy,
|
||||
Muting: r.Muting,
|
||||
MutingNotifications: r.MutingNotifications,
|
||||
Requested: r.Requested,
|
||||
DomainBlocking: r.DomainBlocking,
|
||||
Endorsed: r.Endorsed,
|
||||
Note: r.Note,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -22,6 +22,8 @@ import (
|
|||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -47,6 +49,8 @@ const (
|
|||
FeaturedPath = "featured"
|
||||
// PublicKeyPath is for serving an account's public 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
|
||||
|
@ -103,6 +107,12 @@ type UserURIs struct {
|
|||
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.
|
||||
func GenerateURIsForAccount(username string, protocol string, host string) *UserURIs {
|
||||
// The below URLs are used for serving web requests
|
||||
|
|
|
@ -48,6 +48,7 @@ import (
|
|||
var Run action.GTSAction = func(ctx context.Context, _ *config.Config, log *logrus.Logger) error {
|
||||
c := NewTestConfig()
|
||||
dbService := NewTestDB()
|
||||
federatingDB := NewTestFederatingDB(dbService)
|
||||
router := NewTestRouter()
|
||||
storageBackend := NewTestStorage()
|
||||
|
||||
|
@ -59,7 +60,7 @@ var Run action.GTSAction = func(ctx context.Context, _ *config.Config, log *logr
|
|||
Body: r,
|
||||
}, nil
|
||||
}))
|
||||
federator := federation.NewFederator(dbService, transportController, c, log, typeConverter)
|
||||
federator := federation.NewFederator(dbService, federatingDB, transportController, c, log, typeConverter)
|
||||
processor := NewTestProcessor(dbService, storageBackend, federator)
|
||||
if err := processor.Start(); err != nil {
|
||||
return fmt.Errorf("error starting processor: %s", err)
|
||||
|
|
11
testrig/federatingdb.go
Normal file
11
testrig/federatingdb.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package testrig
|
||||
|
||||
import (
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||
)
|
||||
|
||||
// NewTestFederatingDB returns a federating DB with the underlying db
|
||||
func NewTestFederatingDB(db db.DB) federation.FederatingDB {
|
||||
return federation.NewFederatingDB(db, NewTestConfig(), NewTestLog())
|
||||
}
|
|
@ -26,5 +26,5 @@ import (
|
|||
|
||||
// NewTestFederator returns a federator with the given database and (mock!!) transport controller.
|
||||
func NewTestFederator(db db.DB, tc transport.Controller) federation.Federator {
|
||||
return federation.NewFederator(db, tc, NewTestConfig(), NewTestLog(), NewTestTypeConverter(db))
|
||||
return federation.NewFederator(db, NewTestFederatingDB(db), tc, NewTestConfig(), NewTestLog(), NewTestTypeConverter(db))
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue