summaryrefslogtreecommitdiff
path: root/internal/api
diff options
context:
space:
mode:
authorLibravatar Tobi Smethurst <31960611+tsmethurst@users.noreply.github.com>2021-05-21 15:48:26 +0200
committerLibravatar GitHub <noreply@github.com>2021-05-21 15:48:26 +0200
commitd839f27c306eedebdc7cc0311f35b8856cc2bb24 (patch)
tree7a11a3a641f902991d26771c4d3f8e836a2bce7e /internal/api
parentupdate progress (diff)
downloadgotosocial-d839f27c306eedebdc7cc0311f35b8856cc2bb24.tar.xz
Follows and relationships (#27)
* Follows -- create and undo, both remote and local * Statuses -- federate new posts, including media, attachments, CWs and image descriptions.
Diffstat (limited to 'internal/api')
-rw-r--r--internal/api/client/account/account.go12
-rw-r--r--internal/api/client/account/follow.go56
-rw-r--r--internal/api/client/account/following.go49
-rw-r--r--internal/api/client/account/relationships.go46
-rw-r--r--internal/api/client/account/unfollow.go53
-rw-r--r--internal/api/client/auth/authorize.go3
-rw-r--r--internal/api/client/auth/middleware.go3
-rw-r--r--internal/api/client/auth/signin.go3
-rw-r--r--internal/api/client/followrequest/accept.go5
-rw-r--r--internal/api/client/status/statuscreate_test.go3
-rw-r--r--internal/api/model/account.go51
-rw-r--r--internal/api/s2s/user/followers.go58
-rw-r--r--internal/api/s2s/user/statusget.go46
-rw-r--r--internal/api/s2s/user/user.go8
-rw-r--r--internal/api/security/security.go1
-rw-r--r--internal/api/security/useragentblock.go43
16 files changed, 414 insertions, 26 deletions
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 <http://www.gnu.org/licenses/>.
+*/
+
+package account
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+// AccountFollowPOSTHandler is the endpoint for creating a new follow request to the target account
+func (m *Module) AccountFollowPOSTHandler(c *gin.Context) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
+ return
+ }
+
+ targetAcctID := c.Param(IDKey)
+ if targetAcctID == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
+ return
+ }
+ form := &model.AccountFollowRequest{}
+ if err := c.ShouldBind(form); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+ form.TargetAccountID = targetAcctID
+
+ relationship, errWithCode := m.processor.AccountFollowCreate(authed, form)
+ if errWithCode != nil {
+ c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ return
+ }
+
+ c.JSON(http.StatusOK, relationship)
+}
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 <http://www.gnu.org/licenses/>.
+*/
+
+package account
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+// AccountFollowersGETHandler serves the followers of the requested account, if they're visible to the requester.
+func (m *Module) AccountFollowingGETHandler(c *gin.Context) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
+ return
+ }
+
+ targetAcctID := c.Param(IDKey)
+ if targetAcctID == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
+ return
+ }
+
+ following, errWithCode := m.processor.AccountFollowingGet(authed, targetAcctID)
+ if errWithCode != nil {
+ c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ return
+ }
+
+ c.JSON(http.StatusOK, following)
+}
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 <http://www.gnu.org/licenses/>.
+*/
+
+package account
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+// AccountUnfollowPOSTHandler is the endpoint for removing a follow and/or follow request to the target account
+func (m *Module) AccountUnfollowPOSTHandler(c *gin.Context) {
+ l := m.log.WithField("func", "AccountUnfollowPOSTHandler")
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ l.Debug(err)
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
+ return
+ }
+
+ targetAcctID := c.Param(IDKey)
+ if targetAcctID == "" {
+ l.Debug(err)
+ c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
+ return
+ }
+
+ relationship, errWithCode := m.processor.AccountFollowRemove(authed, targetAcctID)
+ if errWithCode != nil {
+ l.Debug(errWithCode.Error())
+ c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ return
+ }
+
+ c.JSON(http.StatusOK, relationship)
+}
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 := &gtsmodel.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 := &gtsmodel.Application{}
- if err := m.db.GetWhere("client_id", cid, app); err != nil {
+ if err := m.db.GetWhere([]db.Where{{Key: "client_id",Value: cid}}, app); err != nil {
l.Tracef("no app found for client %s", cid)
}
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 := &gtsmodel.User{}
- if err := m.db.GetWhere("email", email, gtsUser); err != nil {
+ if err := m.db.GetWhere([]db.Where{{Key: "email", Value: email}}, gtsUser); err != nil {
l.Debugf("user %s was not retrievable from db during oauth authorization attempt: %s", email, err)
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 := &gtsmodel.Tag{}
- err = suite.db.GetWhere("name", "helloworld", gtsTag)
+ err = suite.db.GetWhere([]db.Where{{Key: "name", Value: "helloworld"}}, gtsTag)
assert.NoError(suite.T(), err)
assert.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 <http://www.gnu.org/licenses/>.
+*/
+
+package user
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ "github.com/sirupsen/logrus"
+)
+
+func (m *Module) FollowersGETHandler(c *gin.Context) {
+ l := m.log.WithFields(logrus.Fields{
+ "func": "FollowersGETHandler",
+ "url": c.Request.RequestURI,
+ })
+
+ requestedUsername := c.Param(UsernameKey)
+ if requestedUsername == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"})
+ return
+ }
+
+ // make sure this actually an AP request
+ format := c.NegotiateFormat(ActivityPubAcceptHeaders...)
+ if format == "" {
+ c.JSON(http.StatusNotAcceptable, gin.H{"error": "could not negotiate format with given Accept header(s)"})
+ return
+ }
+ l.Tracef("negotiated format: %s", format)
+
+ // make a copy of the context to pass along so we don't break anything
+ cp := c.Copy()
+ user, err := m.processor.GetFediFollowers(requestedUsername, cp.Request) // GetFediUser handles auth as well
+ if err != nil {
+ l.Info(err.Error())
+ c.JSON(err.Code(), gin.H{"error": err.Safe()})
+ return
+ }
+
+ c.JSON(http.StatusOK, user)
+}
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 <http://www.gnu.org/licenses/>.
+*/
+
+package security
+
+import (
+ "net/http"
+ "strings"
+
+ "github.com/gin-gonic/gin"
+)
+
+// UserAgentBlock is a middleware that prevents google chrome cohort tracking by
+// writing the Permissions-Policy header after all other parts of the request have been completed.
+// See: https://plausible.io/blog/google-floc
+func (m *Module) UserAgentBlock(c *gin.Context) {
+
+ ua := c.Request.UserAgent()
+ if ua == "" {
+ c.AbortWithStatus(http.StatusTeapot)
+ return
+ }
+
+ if strings.Contains(strings.ToLower(c.Request.UserAgent()), strings.ToLower("friendica")) {
+ c.AbortWithStatus(http.StatusTeapot)
+ return
+ }
+}