From d839f27c306eedebdc7cc0311f35b8856cc2bb24 Mon Sep 17 00:00:00 2001
From: Tobi Smethurst <31960611+tsmethurst@users.noreply.github.com>
Date: Fri, 21 May 2021 15:48:26 +0200
Subject: [PATCH] Follows and relationships (#27)
* Follows -- create and undo, both remote and local
* Statuses -- federate new posts, including media, attachments, CWs and image descriptions.
---
PROGRESS.md | 9 +-
internal/api/client/account/account.go | 12 +
internal/api/client/account/follow.go | 56 +++
internal/api/client/account/following.go | 49 +++
internal/api/client/account/relationships.go | 46 +++
internal/api/client/account/unfollow.go | 53 +++
internal/api/client/auth/authorize.go | 3 +-
internal/api/client/auth/middleware.go | 3 +-
internal/api/client/auth/signin.go | 3 +-
internal/api/client/followrequest/accept.go | 5 +-
.../api/client/status/statuscreate_test.go | 3 +-
internal/api/model/account.go | 51 +--
internal/api/s2s/user/followers.go | 58 ++++
internal/api/s2s/user/statusget.go | 46 +++
internal/api/s2s/user/user.go | 8 +
internal/api/security/security.go | 1 +
internal/api/security/useragentblock.go | 43 +++
internal/db/db.go | 29 +-
internal/db/pg/pg.go | 157 +++++++--
internal/federation/federating_db.go | 327 ++++++++++++++++--
internal/federation/federatingprotocol.go | 45 ++-
internal/federation/federator.go | 12 +-
internal/federation/federator_test.go | 4 +-
internal/federation/util.go | 118 +++++--
internal/gotosocial/actions.go | 4 +-
internal/gtsmodel/account.go | 6 +-
internal/gtsmodel/activitystreams.go | 99 +++---
internal/gtsmodel/mention.go | 10 +
internal/gtsmodel/messages.go | 13 +-
internal/gtsmodel/relationship.go | 49 +++
internal/gtsmodel/status.go | 2 +-
internal/media/handler.go | 48 ++-
internal/media/handler_test.go | 2 +-
internal/media/processicon.go | 4 +-
internal/message/accountprocess.go | 250 +++++++++++++
internal/message/fediprocess.go | 86 ++++-
internal/message/fromclientapiprocess.go | 213 ++++++++++--
internal/message/fromcommonprocess.go | 4 +
internal/message/fromfederatorprocess.go | 94 ++++-
internal/message/frprocess.go | 25 +-
internal/message/instanceprocess.go | 3 +-
internal/message/processor.go | 24 +-
internal/message/processorutil.go | 39 ++-
internal/oauth/tokenstore.go | 12 +-
internal/transport/controller.go | 3 +
internal/transport/transport.go | 8 +
internal/typeutils/astointernal.go | 55 ++-
internal/typeutils/converter.go | 18 +
internal/typeutils/internaltoas.go | 303 +++++++++++++++-
internal/typeutils/internaltofrontend.go | 18 +
internal/util/uri.go | 10 +
testrig/actions.go | 3 +-
testrig/federatingdb.go | 11 +
testrig/federator.go | 2 +-
54 files changed, 2260 insertions(+), 299 deletions(-)
create mode 100644 internal/api/client/account/follow.go
create mode 100644 internal/api/client/account/following.go
create mode 100644 internal/api/client/account/relationships.go
create mode 100644 internal/api/client/account/unfollow.go
create mode 100644 internal/api/s2s/user/followers.go
create mode 100644 internal/api/s2s/user/statusget.go
create mode 100644 internal/api/security/useragentblock.go
create mode 100644 internal/gtsmodel/relationship.go
create mode 100644 testrig/federatingdb.go
diff --git a/PROGRESS.md b/PROGRESS.md
index f00d9b25f..653f2df23 100644
--- a/PROGRESS.md
+++ b/PROGRESS.md
@@ -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
diff --git a/internal/api/client/account/account.go b/internal/api/client/account/account.go
index 1e4b716f5..94f753825 100644
--- a/internal/api/client/account/account.go
+++ b/internal/api/client/account/account.go
@@ -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
}
diff --git a/internal/api/client/account/follow.go b/internal/api/client/account/follow.go
new file mode 100644
index 000000000..bee41c280
--- /dev/null
+++ b/internal/api/client/account/follow.go
@@ -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 .
+*/
+
+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)
+}
diff --git a/internal/api/client/account/following.go b/internal/api/client/account/following.go
new file mode 100644
index 000000000..2a1373e40
--- /dev/null
+++ b/internal/api/client/account/following.go
@@ -0,0 +1,49 @@
+/*
+ GoToSocial
+ Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+package account
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+// 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)
+}
diff --git a/internal/api/client/account/relationships.go b/internal/api/client/account/relationships.go
new file mode 100644
index 000000000..fd96867ac
--- /dev/null
+++ b/internal/api/client/account/relationships.go
@@ -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)
+}
diff --git a/internal/api/client/account/unfollow.go b/internal/api/client/account/unfollow.go
new file mode 100644
index 000000000..69ed72b88
--- /dev/null
+++ b/internal/api/client/account/unfollow.go
@@ -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 .
+*/
+
+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)
+}
diff --git a/internal/api/client/auth/authorize.go b/internal/api/client/auth/authorize.go
index d5f8ee214..f473579db 100644
--- a/internal/api/client/auth/authorize.go
+++ b/internal/api/client/auth/authorize.go
@@ -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
}
diff --git a/internal/api/client/auth/middleware.go b/internal/api/client/auth/middleware.go
index 2a63cbdb6..dba8e5a1d 100644
--- a/internal/api/client/auth/middleware.go
+++ b/internal/api/client/auth/middleware.go
@@ -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)
diff --git a/internal/api/client/auth/signin.go b/internal/api/client/auth/signin.go
index 79d9b300e..e9385e39a 100644
--- a/internal/api/client/auth/signin.go
+++ b/internal/api/client/auth/signin.go
@@ -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()
}
diff --git a/internal/api/client/followrequest/accept.go b/internal/api/client/followrequest/accept.go
index 45dc1a2af..bb2910c8f 100644
--- a/internal/api/client/followrequest/accept.go
+++ b/internal/api/client/followrequest/accept.go
@@ -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)
}
diff --git a/internal/api/client/status/statuscreate_test.go b/internal/api/client/status/statuscreate_test.go
index fb9b48f8a..a78374fe8 100644
--- a/internal/api/client/status/statuscreate_test.go
+++ b/internal/api/client/status/statuscreate_test.go
@@ -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)
}
diff --git a/internal/api/model/account.go b/internal/api/model/account.go
index efb69d6fd..ba5ac567a 100644
--- a/internal/api/model/account.go
+++ b/internal/api/model/account.go
@@ -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"`
}
diff --git a/internal/api/s2s/user/followers.go b/internal/api/s2s/user/followers.go
new file mode 100644
index 000000000..0b633619f
--- /dev/null
+++ b/internal/api/s2s/user/followers.go
@@ -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 .
+*/
+
+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)
+}
diff --git a/internal/api/s2s/user/statusget.go b/internal/api/s2s/user/statusget.go
new file mode 100644
index 000000000..60efd484e
--- /dev/null
+++ b/internal/api/s2s/user/statusget.go
@@ -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)
+}
diff --git a/internal/api/s2s/user/user.go b/internal/api/s2s/user/user.go
index a6116247d..d866e47e1 100644
--- a/internal/api/s2s/user/user.go
+++ b/internal/api/s2s/user/user.go
@@ -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
}
diff --git a/internal/api/security/security.go b/internal/api/security/security.go
index eaae8471e..523b5dd55 100644
--- a/internal/api/security/security.go
+++ b/internal/api/security/security.go
@@ -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
}
diff --git a/internal/api/security/useragentblock.go b/internal/api/security/useragentblock.go
new file mode 100644
index 000000000..f7d3a4ffc
--- /dev/null
+++ b/internal/api/security/useragentblock.go
@@ -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 .
+*/
+
+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
+ }
+}
diff --git a/internal/db/db.go b/internal/db/db.go
index cbcd698c9..5609b926f 100644
--- a/internal/db/db.go
+++ b/internal/db/db.go
@@ -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)
diff --git a/internal/db/pg/pg.go b/internal/db/pg/pg.go
index d3590a027..2a4b040d1 100644
--- a/internal/db/pg/pg.go
+++ b/internal/db/pg/pg.go
@@ -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"
@@ -46,11 +44,11 @@ import (
// postgresService satisfies the DB interface
type postgresService struct {
- config *config.Config
- conn *pg.DB
- log *logrus.Logger
- cancel context.CancelFunc
- federationDB pub.Database
+ config *config.Config
+ conn *pg.DB
+ log *logrus.Logger
+ cancel context.CancelFunc
+ // 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,20 +284,31 @@ 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 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 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
@@ -1093,9 +1170,13 @@ func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, ori
// id, createdAt and updatedAt will be populated by the db, so we have everything we need!
menchies = append(menchies, >smodel.Mention{
- StatusID: statusID,
- OriginAccountID: originAccountID,
- TargetAccountID: mentionedAccount.ID,
+ StatusID: statusID,
+ OriginAccountID: ogAccount.ID,
+ OriginAccountURI: ogAccount.URI,
+ TargetAccountID: mentionedAccount.ID,
+ NameString: a,
+ MentionedAccountURI: mentionedAccount.URI,
+ GTSAccount: mentionedAccount,
})
}
return menchies, nil
diff --git a/internal/federation/federating_db.go b/internal/federation/federating_db.go
index f72c5e636..8f203e132 100644
--- a/internal/federation/federating_db.go
+++ b/internal/federation/federating_db.go
@@ -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,13 +428,17 @@ 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)
}
fromFederatorChan <- gtsmodel.FromFederator{
- APObjectType: gtsmodel.ActivityStreamsNote,
- APActivityType: gtsmodel.ActivityStreamsCreate,
- GTSModel: status,
+ 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
+}
diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go
index d8f6eb839..61fecb11a 100644
--- a/internal/federation/federatingprotocol.go
+++ b/internal/federation/federatingprotocol.go
@@ -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
}
diff --git a/internal/federation/federator.go b/internal/federation/federator.go
index a3b1386e4..f09a77279 100644
--- a/internal/federation/federator.go
+++ b/internal/federation/federator.go
@@ -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
+}
diff --git a/internal/federation/federator_test.go b/internal/federation/federator_test.go
index 2eab09507..e5d42b53d 100644
--- a/internal/federation/federator_test.go
+++ b/internal/federation/federator_test.go
@@ -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()
diff --git a/internal/federation/util.go b/internal/federation/util.go
index 14ceaeb1d..3f53ed6a7 100644
--- a/internal/federation/util.go
+++ b/internal/federation/util.go
@@ -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,57 +130,73 @@ func (f *federator) AuthenticateFederatedRequest(username string, r *http.Reques
return nil, fmt.Errorf("could not parse key id into a url: %s", err)
}
- transport, err := f.GetTransportForUser(username)
- if err != nil {
- return nil, fmt.Errorf("transport err: %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)
+ }
- // The actual http call to the remote server is made right here in the Dereference function.
- b, err := transport.Dereference(context.Background(), requestingPublicKeyID)
- if err != nil {
- return nil, fmt.Errorf("error deferencing key %s: %s", requestingPublicKeyID.String(), err)
- }
+ // The actual http call to the remote server is made right here in the Dereference function.
+ b, err := transport.Dereference(context.Background(), requestingPublicKeyID)
+ if err != nil {
+ return nil, fmt.Errorf("error deferencing key %s: %s", requestingPublicKeyID.String(), err)
+ }
- // if the key isn't in the response, we can't authenticate the request
- requestingPublicKey, err := getPublicKeyFromResponse(context.Background(), b, requestingPublicKeyID)
- if err != nil {
- return nil, fmt.Errorf("error getting key %s from response %s: %s", requestingPublicKeyID.String(), string(b), err)
- }
+ // if the key isn't in the response, we can't authenticate the request
+ requestingPublicKey, err := getPublicKeyFromResponse(context.Background(), b, requestingPublicKeyID)
+ if err != nil {
+ return nil, fmt.Errorf("error getting key %s from response %s: %s", requestingPublicKeyID.String(), string(b), err)
+ }
- // we should be able to get the actual key embedded in the vocab.W3IDSecurityV1PublicKey
- pkPemProp := requestingPublicKey.GetW3IDSecurityV1PublicKeyPem()
- if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() {
- return nil, errors.New("publicKeyPem property is not provided or it is not embedded as a value")
- }
+ // we should be able to get the actual key embedded in the vocab.W3IDSecurityV1PublicKey
+ pkPemProp := requestingPublicKey.GetW3IDSecurityV1PublicKeyPem()
+ if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() {
+ return nil, errors.New("publicKeyPem property is not provided or it is not embedded as a value")
+ }
- // and decode the PEM so that we can parse it as a golang public key
- pubKeyPem := pkPemProp.Get()
- block, _ := pem.Decode([]byte(pubKeyPem))
- if block == nil || block.Type != "PUBLIC KEY" {
- return nil, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type")
- }
+ // and decode the PEM so that we can parse it as a golang public key
+ pubKeyPem := pkPemProp.Get()
+ block, _ := pem.Decode([]byte(pubKeyPem))
+ if block == nil || block.Type != "PUBLIC KEY" {
+ return nil, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type")
+ }
- p, err := x509.ParsePKIXPublicKey(block.Bytes)
- if err != nil {
- return nil, fmt.Errorf("could not parse public key from block bytes: %s", err)
+ publicKey, err = x509.ParsePKIXPublicKey(block.Bytes)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse public key from block bytes: %s", err)
+ }
+
+ // all good! we just need the URI of the key owner to return
+ pkOwnerProp := requestingPublicKey.GetW3IDSecurityV1Owner()
+ if pkOwnerProp == nil || !pkOwnerProp.IsIRI() {
+ return nil, errors.New("publicKeyOwner property is not provided or it is not embedded as a value")
+ }
+ pkOwnerURI = pkOwnerProp.GetIRI()
}
- if p == nil {
+ if publicKey == nil {
return nil, errors.New("returned public key was empty")
}
// do the actual authentication here!
algo := httpsig.RSA_SHA256 // TODO: make this more robust
- if err := verifier.Verify(p, algo); err != nil {
+ if err := verifier.Verify(publicKey, algo); err != nil {
return nil, fmt.Errorf("error verifying key %s: %s", requestingPublicKeyID.String(), err)
}
- // all good! we just need the URI of the key owner to return
- pkOwnerProp := requestingPublicKey.GetW3IDSecurityV1Owner()
- if pkOwnerProp == nil || !pkOwnerProp.IsIRI() {
- return nil, errors.New("publicKeyOwner property is not provided or it is not embedded as a value")
- }
- pkOwnerURI := pkOwnerProp.GetIRI()
-
return pkOwnerURI, nil
}
@@ -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
+}
diff --git a/internal/gotosocial/actions.go b/internal/gotosocial/actions.go
index 94b29b883..557a39626 100644
--- a/internal/gotosocial/actions.go
+++ b/internal/gotosocial/actions.go
@@ -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)
diff --git a/internal/gtsmodel/account.go b/internal/gtsmodel/account.go
index 56c401e62..d6ce95cc9 100644
--- a/internal/gtsmodel/account.go
+++ b/internal/gtsmodel/account.go
@@ -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
diff --git a/internal/gtsmodel/activitystreams.go b/internal/gtsmodel/activitystreams.go
index f852340bb..3fe6107b5 100644
--- a/internal/gtsmodel/activitystreams.go
+++ b/internal/gtsmodel/activitystreams.go
@@ -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"
)
diff --git a/internal/gtsmodel/mention.go b/internal/gtsmodel/mention.go
index 8e56a1b36..3abe9c915 100644
--- a/internal/gtsmodel/mention.go
+++ b/internal/gtsmodel/mention.go
@@ -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:"-"`
}
diff --git a/internal/gtsmodel/messages.go b/internal/gtsmodel/messages.go
index 43f30634a..910c74898 100644
--- a/internal/gtsmodel/messages.go
+++ b/internal/gtsmodel/messages.go
@@ -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
- GTSModel interface{}
+ APObjectType string
+ APActivityType string
+ GTSModel interface{}
+ ReceivingAccount *Account
}
diff --git a/internal/gtsmodel/relationship.go b/internal/gtsmodel/relationship.go
new file mode 100644
index 000000000..4e6cc03f6
--- /dev/null
+++ b/internal/gtsmodel/relationship.go
@@ -0,0 +1,49 @@
+/*
+ GoToSocial
+ Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+package 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
+}
diff --git a/internal/gtsmodel/status.go b/internal/gtsmodel/status.go
index d0d479520..b5ac8def1 100644
--- a/internal/gtsmodel/status.go
+++ b/internal/gtsmodel/status.go
@@ -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?
diff --git a/internal/media/handler.go b/internal/media/handler.go
index 8bbff9c46..b59e836ed 100644
--- a/internal/media/handler.go
+++ b/internal/media/handler.go
@@ -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)
+}
diff --git a/internal/media/handler_test.go b/internal/media/handler_test.go
index 03dcdc21d..02bf334c5 100644
--- a/internal/media/handler_test.go
+++ b/internal/media/handler_test.go
@@ -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)
diff --git a/internal/media/processicon.go b/internal/media/processicon.go
index 962d1c6d8..bc2c55809 100644
--- a/internal/media/processicon.go
+++ b/internal/media/processicon.go
@@ -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,
diff --git a/internal/message/accountprocess.go b/internal/message/accountprocess.go
index a10f6d016..29fd55034 100644
--- a/internal/message/accountprocess.go
+++ b/internal/message/accountprocess.go
@@ -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)
+}
diff --git a/internal/message/fediprocess.go b/internal/message/fediprocess.go
index 3c7c30e27..eb6e8b6d6 100644
--- a/internal/message/fediprocess.go
+++ b/internal/message/fediprocess.go
@@ -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{}
diff --git a/internal/message/fromclientapiprocess.go b/internal/message/fromclientapiprocess.go
index 1a12216e7..e91bd6ce4 100644
--- a/internal/message/fromclientapiprocess.go
+++ b/internal/message/fromclientapiprocess.go
@@ -19,55 +19,198 @@
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.APObjectType {
- case gtsmodel.ActivityStreamsNote:
- status, ok := clientMsg.GTSModel.(*gtsmodel.Status)
- if !ok {
- return errors.New("note was not parseable as *gtsmodel.Status")
- }
+ 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")
+ }
- if err := p.notifyStatus(status); err != nil {
- return err
- }
+ if err := p.notifyStatus(status); err != nil {
+ return err
+ }
- if status.VisibilityAdvanced.Federated {
- return p.federateStatus(status)
+ if status.VisibilityAdvanced.Federated {
+ return p.federateStatus(status)
+ }
+ return nil
+ case gtsmodel.ActivityStreamsFollow:
+ // CREATE FOLLOW (request)
+ follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow)
+ if !ok {
+ return errors.New("follow was not parseable as *gtsmodel.Follow")
+ }
+
+ if err := p.notifyFollow(follow); err != nil {
+ return err
+ }
+
+ return p.federateFollow(follow, clientMsg.OriginAccount, clientMsg.TargetAccount)
+ }
+ case gtsmodel.ActivityStreamsUpdate:
+ // UPDATE
+ case gtsmodel.ActivityStreamsAccept:
+ // ACCEPT
+ switch clientMsg.APObjectType {
+ case gtsmodel.ActivityStreamsFollow:
+ // ACCEPT FOLLOW
+ follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow)
+ if !ok {
+ return errors.New("accept was not parseable as *gtsmodel.Follow")
+ }
+ return p.federateAcceptFollowRequest(follow, clientMsg.OriginAccount, clientMsg.TargetAccount)
+ }
+ case gtsmodel.ActivityStreamsUndo:
+ // UNDO
+ switch clientMsg.APObjectType {
+ case gtsmodel.ActivityStreamsFollow:
+ // UNDO FOLLOW
+ follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow)
+ if !ok {
+ return errors.New("undo was not parseable as *gtsmodel.Follow")
+ }
+ return p.federateUnfollow(follow, clientMsg.OriginAccount, clientMsg.TargetAccount)
}
- return nil
}
- return fmt.Errorf("message type unprocessable: %+v", clientMsg)
+ return nil
}
func (p *processor) federateStatus(status *gtsmodel.Status) error {
- // // 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
}
diff --git a/internal/message/fromcommonprocess.go b/internal/message/fromcommonprocess.go
index 14f145df9..d557b7962 100644
--- a/internal/message/fromcommonprocess.go
+++ b/internal/message/fromcommonprocess.go
@@ -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
+}
diff --git a/internal/message/fromfederatorprocess.go b/internal/message/fromfederatorprocess.go
index 2dd8e9e3b..ffaa1b93b 100644
--- a/internal/message/fromfederatorprocess.go
+++ b/internal/message/fromfederatorprocess.go
@@ -38,24 +38,60 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
l.Debug("entering function PROCESS FROM FEDERATOR")
- switch federatorMsg.APObjectType {
- case gtsmodel.ActivityStreamsNote:
+ 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")
+ }
- incomingStatus, ok := federatorMsg.GTSModel.(*gtsmodel.Status)
- if !ok {
- return errors.New("note was not parseable as *gtsmodel.Status")
- }
+ l.Debug("will now derefence incoming status")
+ if err := p.dereferenceStatusFields(incomingStatus); err != nil {
+ return fmt.Errorf("error dereferencing status from federator: %s", err)
+ }
+ if err := p.db.UpdateByID(incomingStatus.ID, incomingStatus); err != nil {
+ return fmt.Errorf("error updating dereferenced status in the db: %s", err)
+ }
- l.Debug("will now derefence incoming status")
- if err := p.dereferenceStatusFields(incomingStatus); err != nil {
- return fmt.Errorf("error dereferencing status from federator: %s", err)
- }
- if err := p.db.UpdateByID(incomingStatus.ID, incomingStatus); err != nil {
- return fmt.Errorf("error updating dereferenced status in the db: %s", err)
- }
+ 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")
+ }
- if err := p.notifyStatus(incomingStatus); err != nil {
- return err
+ 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)
+ }
}
}
@@ -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
+}
diff --git a/internal/message/frprocess.go b/internal/message/frprocess.go
index cc3838598..fd64b4c50 100644
--- a/internal/message/frprocess.go
+++ b/internal/message/frprocess.go
@@ -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 {
diff --git a/internal/message/instanceprocess.go b/internal/message/instanceprocess.go
index 05ea103fd..f544fbf30 100644
--- a/internal/message/instanceprocess.go
+++ b/internal/message/instanceprocess.go
@@ -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))
}
diff --git a/internal/message/processor.go b/internal/message/processor.go
index c9ba5f858..e9888d647 100644
--- a/internal/message/processor.go
+++ b/internal/message/processor.go
@@ -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 {
diff --git a/internal/message/processorutil.go b/internal/message/processorutil.go
index 676635a51..67c96abe0 100644
--- a/internal/message/processorutil.go
+++ b/internal/message/processorutil.go
@@ -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
+}
diff --git a/internal/oauth/tokenstore.go b/internal/oauth/tokenstore.go
index 195db838f..04319ee0b 100644
--- a/internal/oauth/tokenstore.go
+++ b/internal/oauth/tokenstore.go
@@ -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
diff --git a/internal/transport/controller.go b/internal/transport/controller.go
index ad754080a..c01af0900 100644
--- a/internal/transport/controller.go
+++ b/internal/transport/controller.go
@@ -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
}
diff --git a/internal/transport/transport.go b/internal/transport/transport.go
index afd408519..4fba484cd 100644
--- a/internal/transport/transport.go
+++ b/internal/transport/transport.go
@@ -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
diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go
index 4aa6e2b19..bf1ef7f45 100644
--- a/internal/typeutils/astointernal.go
+++ b/internal/typeutils/astointernal.go
@@ -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") {
diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go
index 8f310c921..ac2ce4317 100644
--- a/internal/typeutils/converter.go
+++ b/internal/typeutils/converter.go
@@ -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 {
diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go
index 0216dea5e..072c4e690 100644
--- a/internal/typeutils/internaltoas.go
+++ b/internal/typeutils/internaltoas.go
@@ -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
}
diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go
index e4ccab988..70f8a8d3c 100644
--- a/internal/typeutils/internaltofrontend.go
+++ b/internal/typeutils/internaltofrontend.go
@@ -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
+}
diff --git a/internal/util/uri.go b/internal/util/uri.go
index edcfc5c02..cee9dcbaa 100644
--- a/internal/util/uri.go
+++ b/internal/util/uri.go
@@ -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
diff --git a/testrig/actions.go b/testrig/actions.go
index 7ed75b18f..aa78799b8 100644
--- a/testrig/actions.go
+++ b/testrig/actions.go
@@ -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)
diff --git a/testrig/federatingdb.go b/testrig/federatingdb.go
new file mode 100644
index 000000000..5cce24752
--- /dev/null
+++ b/testrig/federatingdb.go
@@ -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())
+}
diff --git a/testrig/federator.go b/testrig/federator.go
index c2d86fd21..e113c43b4 100644
--- a/testrig/federator.go
+++ b/testrig/federator.go
@@ -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))
}