summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorLibravatar tobi <tobi.smethurst@protonmail.com>2025-10-15 18:57:57 +0200
committerLibravatar tobi <tobi.smethurst@protonmail.com>2025-10-17 15:33:49 +0200
commit6fee55dcff976f3eeae5879fe91d2f27780d0da4 (patch)
treed028c3ac30a84fc6095c9ca9dd4d136f905d8887 /internal
parent[bugfix] Fix HTTP return code for Likes of remote statuses (#4504) (diff)
downloadgotosocial-6fee55dcff976f3eeae5879fe91d2f27780d0da4.tar.xz
[chore] Rationalize HTTP return codes for fedi endpoints, other tidying up (#4503)
# Description > If this is a code change, please include a summary of what you've coded, and link to the issue(s) it closes/implements. > > If this is a documentation change, please briefly describe what you've changed and why. This pull request does some refactoring of the fedi API endpoints and processing functions, and the authenticate + pub key deref functions, to try to return fewer silly HTTP codes like 410 Gone (when a *remote* account is gone, not a local one), and 500 errors where something isn't really an error. Also does some general tidying up and renaming for consistency. ## Checklist Please put an x inside each checkbox to indicate that you've read and followed it: `[ ]` -> `[x]` If this is a documentation change, only the first checkbox must be filled (you can delete the others if you want). - [x] I/we have read the [GoToSocial contribution guidelines](https://codeberg.org/superseriousbusiness/gotosocial/src/branch/main/CONTRIBUTING.md). - [x] I/we have discussed the proposed changes already, either in an issue on the repository, or in the Matrix chat. - [x] I/we have not leveraged AI to create the proposed changes. - [x] I/we have performed a self-review of added code. - [x] I/we have written code that is legible and maintainable by others. - [x] I/we have commented the added code, particularly in hard-to-understand areas. - [ ] I/we have made any necessary changes to documentation. - [ ] I/we have added tests that cover new code. - [x] I/we have run tests and they pass locally with the changes. - [x] I/we have run `go fmt ./...` and `golangci-lint run`. Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4503 Co-authored-by: tobi <tobi.smethurst@protonmail.com> Co-committed-by: tobi <tobi.smethurst@protonmail.com>
Diffstat (limited to 'internal')
-rw-r--r--internal/api/activitypub/emoji/emoji.go8
-rw-r--r--internal/api/activitypub/emoji/emojiget.go6
-rw-r--r--internal/api/activitypub/emoji/emojiget_test.go3
-rw-r--r--internal/api/activitypub/publickey/publickey.go9
-rw-r--r--internal/api/activitypub/publickey/publickeyget.go14
-rw-r--r--internal/api/activitypub/users/featured.go8
-rw-r--r--internal/api/activitypub/users/followers.go8
-rw-r--r--internal/api/activitypub/users/following.go8
-rw-r--r--internal/api/activitypub/users/inboxpost_test.go4
-rw-r--r--internal/api/activitypub/users/outboxget.go8
-rw-r--r--internal/api/activitypub/users/outboxget_test.go18
-rw-r--r--internal/api/activitypub/users/repliesget.go10
-rw-r--r--internal/api/activitypub/users/repliesget_test.go20
-rw-r--r--internal/api/activitypub/users/statusget.go10
-rw-r--r--internal/api/activitypub/users/statusget_test.go14
-rw-r--r--internal/api/activitypub/users/user.go42
-rw-r--r--internal/api/activitypub/users/userget.go14
-rw-r--r--internal/api/activitypub/users/userget_test.go9
-rw-r--r--internal/api/wellknown/webfinger/webfingerget.go4
-rw-r--r--internal/federation/authenticate.go159
-rw-r--r--internal/federation/federatingactor.go26
-rw-r--r--internal/federation/federatingprotocol.go73
-rw-r--r--internal/federation/gone.go41
-rw-r--r--internal/gtsmodel/account.go6
-rw-r--r--internal/processing/fedi/accept.go57
-rw-r--r--internal/processing/fedi/authorization.go87
-rw-r--r--internal/processing/fedi/collections.go85
-rw-r--r--internal/processing/fedi/common.go107
-rw-r--r--internal/processing/fedi/emoji.go57
-rw-r--r--internal/processing/fedi/status.go36
-rw-r--r--internal/processing/fedi/user.go123
-rw-r--r--internal/processing/fedi/wellknown.go9
-rw-r--r--internal/web/customcss.go4
-rw-r--r--internal/web/profile.go38
-rw-r--r--internal/web/thread.go69
35 files changed, 624 insertions, 570 deletions
diff --git a/internal/api/activitypub/emoji/emoji.go b/internal/api/activitypub/emoji/emoji.go
index 361a3bbc7..e218af9ad 100644
--- a/internal/api/activitypub/emoji/emoji.go
+++ b/internal/api/activitypub/emoji/emoji.go
@@ -20,16 +20,12 @@ package emoji
import (
"net/http"
+ apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
"code.superseriousbusiness.org/gotosocial/internal/processing"
"github.com/gin-gonic/gin"
)
-const (
- // EmojiIDKey is for emoji IDs
- EmojiIDKey = "id"
- // EmojiBasePath is the base path for serving AP Emojis, minus the "emoji" prefix
- EmojiWithIDPath = "/:" + EmojiIDKey
-)
+const EmojiWithIDPath = "/:" + apiutil.IDKey
type Module struct {
processor *processing.Processor
diff --git a/internal/api/activitypub/emoji/emojiget.go b/internal/api/activitypub/emoji/emojiget.go
index cc0049f89..fc6740c9b 100644
--- a/internal/api/activitypub/emoji/emojiget.go
+++ b/internal/api/activitypub/emoji/emojiget.go
@@ -28,8 +28,8 @@ import (
)
func (m *Module) EmojiGetHandler(c *gin.Context) {
- requestedEmojiID := strings.ToUpper(c.Param(EmojiIDKey))
- if requestedEmojiID == "" {
+ emojiID := strings.ToUpper(c.Param(apiutil.IDKey))
+ if emojiID == "" {
err := errors.New("no emoji id specified in request")
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
return
@@ -41,7 +41,7 @@ func (m *Module) EmojiGetHandler(c *gin.Context) {
return
}
- resp, errWithCode := m.processor.Fedi().EmojiGet(c.Request.Context(), requestedEmojiID)
+ resp, errWithCode := m.processor.Fedi().EmojiGet(c.Request.Context(), emojiID)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
diff --git a/internal/api/activitypub/emoji/emojiget_test.go b/internal/api/activitypub/emoji/emojiget_test.go
index 071cb80b6..9cc13f978 100644
--- a/internal/api/activitypub/emoji/emojiget_test.go
+++ b/internal/api/activitypub/emoji/emojiget_test.go
@@ -25,6 +25,7 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/admin"
"code.superseriousbusiness.org/gotosocial/internal/api/activitypub/emoji"
+ apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
"code.superseriousbusiness.org/gotosocial/internal/db"
"code.superseriousbusiness.org/gotosocial/internal/email"
"code.superseriousbusiness.org/gotosocial/internal/federation"
@@ -122,7 +123,7 @@ func (suite *EmojiGetTestSuite) TestGetEmoji() {
// but because we're calling the function directly, we need to set them manually.
ctx.Params = gin.Params{
gin.Param{
- Key: emoji.EmojiIDKey,
+ Key: apiutil.IDKey,
Value: targetEmoji.ID,
},
}
diff --git a/internal/api/activitypub/publickey/publickey.go b/internal/api/activitypub/publickey/publickey.go
index 161123048..eaca9a39b 100644
--- a/internal/api/activitypub/publickey/publickey.go
+++ b/internal/api/activitypub/publickey/publickey.go
@@ -20,17 +20,14 @@ package publickey
import (
"net/http"
+ apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
+
"code.superseriousbusiness.org/gotosocial/internal/processing"
"code.superseriousbusiness.org/gotosocial/internal/uris"
"github.com/gin-gonic/gin"
)
-const (
- // UsernameKey is for account usernames.
- UsernameKey = "username"
- // PublicKeyPath is a path to a user's public key, for serving bare minimum AP representations.
- PublicKeyPath = "users/:" + UsernameKey + "/" + uris.PublicKeyPath
-)
+const PublicKeyPath = "users/:" + apiutil.UsernameKey + "/" + uris.PublicKeyPath
type Module struct {
processor *processing.Processor
diff --git a/internal/api/activitypub/publickey/publickeyget.go b/internal/api/activitypub/publickey/publickeyget.go
index 6e050a3b5..f1b07c2d2 100644
--- a/internal/api/activitypub/publickey/publickeyget.go
+++ b/internal/api/activitypub/publickey/publickeyget.go
@@ -34,8 +34,8 @@ import (
// public key, username, and type of the account.
func (m *Module) PublicKeyGETHandler(c *gin.Context) {
// usernames on our instance are always lowercase
- requestedUsername := strings.ToLower(c.Param(UsernameKey))
- if requestedUsername == "" {
+ requestedUser := strings.ToLower(c.Param(apiutil.UsernameKey))
+ if requestedUser == "" {
err := errors.New("no username specified in request")
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
return
@@ -47,13 +47,17 @@ func (m *Module) PublicKeyGETHandler(c *gin.Context) {
return
}
+ // If HTML is requested, redirect
+ // to user's profile instead.
if contentType == string(apiutil.TextHTML) {
- // redirect to the user's profile
- c.Redirect(http.StatusSeeOther, "/@"+requestedUsername)
+ c.Redirect(http.StatusSeeOther, "/@"+requestedUser)
return
}
- resp, errWithCode := m.processor.Fedi().UserGet(c.Request.Context(), requestedUsername, c.Request.URL)
+ resp, errWithCode := m.processor.Fedi().UserGetMinimal(
+ c.Request.Context(),
+ requestedUser,
+ )
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
diff --git a/internal/api/activitypub/users/featured.go b/internal/api/activitypub/users/featured.go
index 90b368b26..021e5aeb9 100644
--- a/internal/api/activitypub/users/featured.go
+++ b/internal/api/activitypub/users/featured.go
@@ -67,8 +67,8 @@ import (
// description: not found
func (m *Module) FeaturedCollectionGETHandler(c *gin.Context) {
// usernames on our instance are always lowercase
- requestedUsername := strings.ToLower(c.Param(UsernameKey))
- if requestedUsername == "" {
+ requestedUser := strings.ToLower(c.Param(apiutil.UsernameKey))
+ if requestedUser == "" {
err := errors.New("no username specified in request")
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
return
@@ -83,11 +83,11 @@ func (m *Module) FeaturedCollectionGETHandler(c *gin.Context) {
if contentType == string(apiutil.TextHTML) {
// This isn't an ActivityPub request;
// redirect to the user's profile.
- c.Redirect(http.StatusSeeOther, "/@"+requestedUsername)
+ c.Redirect(http.StatusSeeOther, "/@"+requestedUser)
return
}
- resp, errWithCode := m.processor.Fedi().FeaturedCollectionGet(c.Request.Context(), requestedUsername)
+ resp, errWithCode := m.processor.Fedi().FeaturedCollectionGet(c.Request.Context(), requestedUser)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
diff --git a/internal/api/activitypub/users/followers.go b/internal/api/activitypub/users/followers.go
index f00b7bc40..4815df944 100644
--- a/internal/api/activitypub/users/followers.go
+++ b/internal/api/activitypub/users/followers.go
@@ -31,8 +31,8 @@ import (
// FollowersGETHandler returns a collection of URIs for followers of the target user, formatted so that other AP servers can understand it.
func (m *Module) FollowersGETHandler(c *gin.Context) {
// usernames on our instance are always lowercase
- requestedUsername := strings.ToLower(c.Param(UsernameKey))
- if requestedUsername == "" {
+ requestedUser := strings.ToLower(c.Param(apiutil.UsernameKey))
+ if requestedUser == "" {
err := errors.New("no username specified in request")
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
return
@@ -47,7 +47,7 @@ func (m *Module) FollowersGETHandler(c *gin.Context) {
if contentType == string(apiutil.TextHTML) {
// This isn't an ActivityPub request;
// redirect to the user's profile.
- c.Redirect(http.StatusSeeOther, "/@"+requestedUsername)
+ c.Redirect(http.StatusSeeOther, "/@"+requestedUser)
return
}
@@ -61,7 +61,7 @@ func (m *Module) FollowersGETHandler(c *gin.Context) {
return
}
- resp, errWithCode := m.processor.Fedi().FollowersGet(c.Request.Context(), requestedUsername, page)
+ resp, errWithCode := m.processor.Fedi().FollowersGet(c.Request.Context(), requestedUser, page)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
diff --git a/internal/api/activitypub/users/following.go b/internal/api/activitypub/users/following.go
index 630e0b821..ccc5ce641 100644
--- a/internal/api/activitypub/users/following.go
+++ b/internal/api/activitypub/users/following.go
@@ -31,8 +31,8 @@ import (
// FollowingGETHandler returns a collection of URIs for accounts that the target user follows, formatted so that other AP servers can understand it.
func (m *Module) FollowingGETHandler(c *gin.Context) {
// usernames on our instance are always lowercase
- requestedUsername := strings.ToLower(c.Param(UsernameKey))
- if requestedUsername == "" {
+ requestedUser := strings.ToLower(c.Param(apiutil.UsernameKey))
+ if requestedUser == "" {
err := errors.New("no username specified in request")
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
return
@@ -47,7 +47,7 @@ func (m *Module) FollowingGETHandler(c *gin.Context) {
if contentType == string(apiutil.TextHTML) {
// This isn't an ActivityPub request;
// redirect to the user's profile.
- c.Redirect(http.StatusSeeOther, "/@"+requestedUsername)
+ c.Redirect(http.StatusSeeOther, "/@"+requestedUser)
return
}
@@ -61,7 +61,7 @@ func (m *Module) FollowingGETHandler(c *gin.Context) {
return
}
- resp, errWithCode := m.processor.Fedi().FollowingGet(c.Request.Context(), requestedUsername, page)
+ resp, errWithCode := m.processor.Fedi().FollowingGet(c.Request.Context(), requestedUser, page)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
diff --git a/internal/api/activitypub/users/inboxpost_test.go b/internal/api/activitypub/users/inboxpost_test.go
index 51df35672..facf2b459 100644
--- a/internal/api/activitypub/users/inboxpost_test.go
+++ b/internal/api/activitypub/users/inboxpost_test.go
@@ -31,7 +31,7 @@ import (
"code.superseriousbusiness.org/activity/streams"
"code.superseriousbusiness.org/activity/streams/vocab"
"code.superseriousbusiness.org/gotosocial/internal/ap"
- "code.superseriousbusiness.org/gotosocial/internal/api/activitypub/users"
+ apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
"code.superseriousbusiness.org/gotosocial/internal/db"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
@@ -79,7 +79,7 @@ func (suite *InboxPostTestSuite) inboxPost(
)
// Put the request together.
- ctx.AddParam(users.UsernameKey, targetAccount.Username)
+ ctx.AddParam(apiutil.UsernameKey, targetAccount.Username)
ctx.Request = httptest.NewRequest(http.MethodPost, targetAccount.InboxURI, bytes.NewReader(b))
ctx.Request.Header.Set("Signature", signature)
ctx.Request.Header.Set("Date", dateHeader)
diff --git a/internal/api/activitypub/users/outboxget.go b/internal/api/activitypub/users/outboxget.go
index 64bcc195b..a76834ce0 100644
--- a/internal/api/activitypub/users/outboxget.go
+++ b/internal/api/activitypub/users/outboxget.go
@@ -84,8 +84,8 @@ import (
// description: not found
func (m *Module) OutboxGETHandler(c *gin.Context) {
// usernames on our instance are always lowercase
- requestedUsername := strings.ToLower(c.Param(UsernameKey))
- if requestedUsername == "" {
+ requestedUser := strings.ToLower(c.Param(apiutil.UsernameKey))
+ if requestedUser == "" {
err := errors.New("no username specified in request")
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
return
@@ -100,7 +100,7 @@ func (m *Module) OutboxGETHandler(c *gin.Context) {
if contentType == string(apiutil.TextHTML) {
// This isn't an ActivityPub request;
// redirect to the user's profile.
- c.Redirect(http.StatusSeeOther, "/@"+requestedUsername)
+ c.Redirect(http.StatusSeeOther, "/@"+requestedUser)
return
}
@@ -114,7 +114,7 @@ func (m *Module) OutboxGETHandler(c *gin.Context) {
return
}
- resp, errWithCode := m.processor.Fedi().OutboxGet(c.Request.Context(), requestedUsername, page)
+ resp, errWithCode := m.processor.Fedi().OutboxGet(c.Request.Context(), requestedUser, page)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
diff --git a/internal/api/activitypub/users/outboxget_test.go b/internal/api/activitypub/users/outboxget_test.go
index 1290830b2..23d139182 100644
--- a/internal/api/activitypub/users/outboxget_test.go
+++ b/internal/api/activitypub/users/outboxget_test.go
@@ -28,7 +28,7 @@ import (
"code.superseriousbusiness.org/activity/streams"
"code.superseriousbusiness.org/activity/streams/vocab"
- "code.superseriousbusiness.org/gotosocial/internal/api/activitypub/users"
+ apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
"code.superseriousbusiness.org/gotosocial/testrig"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/suite"
@@ -59,7 +59,7 @@ func (suite *OutboxGetTestSuite) TestGetOutbox() {
// but because we're calling the function directly, we need to set them manually.
ctx.Params = gin.Params{
gin.Param{
- Key: users.UsernameKey,
+ Key: apiutil.UsernameKey,
Value: targetAccount.Username,
},
}
@@ -85,7 +85,7 @@ func (suite *OutboxGetTestSuite) TestGetOutbox() {
"type": "OrderedCollection"
}`, dst.String())
- m := make(map[string]interface{})
+ m := make(map[string]any)
err = json.Unmarshal(b, &m)
suite.NoError(err)
@@ -117,7 +117,7 @@ func (suite *OutboxGetTestSuite) TestGetOutboxFirstPage() {
// but because we're calling the function directly, we need to set them manually.
ctx.Params = gin.Params{
gin.Param{
- Key: users.UsernameKey,
+ Key: apiutil.UsernameKey,
Value: targetAccount.Username,
},
}
@@ -172,7 +172,7 @@ func (suite *OutboxGetTestSuite) TestGetOutboxFirstPage() {
"type": "OrderedCollectionPage"
}`, dst.String())
- m := make(map[string]interface{})
+ m := make(map[string]any)
err = json.Unmarshal(b, &m)
suite.NoError(err)
@@ -204,11 +204,11 @@ func (suite *OutboxGetTestSuite) TestGetOutboxNextPage() {
// but because we're calling the function directly, we need to set them manually.
ctx.Params = gin.Params{
gin.Param{
- Key: users.UsernameKey,
+ Key: apiutil.UsernameKey,
Value: targetAccount.Username,
},
gin.Param{
- Key: users.MaxIDKey,
+ Key: apiutil.MaxIDKey,
Value: "01F8MHAMCHF6Y650WCRSCP4WMY",
},
}
@@ -235,7 +235,7 @@ func (suite *OutboxGetTestSuite) TestGetOutboxNextPage() {
"type": "OrderedCollectionPage"
}`, dst.String())
- m := make(map[string]interface{})
+ m := make(map[string]any)
err = json.Unmarshal(b, &m)
suite.NoError(err)
@@ -261,7 +261,7 @@ func checkDropPublished(t *testing.T, b []byte, at ...string) []byte {
entries := make([]map[string]any, 0)
for _, key := range at {
switch vt := m[key].(type) {
- case []interface{}:
+ case []any:
for _, t := range vt {
if entry, ok := t.(map[string]any); ok {
entries = append(entries, entry)
diff --git a/internal/api/activitypub/users/repliesget.go b/internal/api/activitypub/users/repliesget.go
index 3a8a81bfb..facbbcc85 100644
--- a/internal/api/activitypub/users/repliesget.go
+++ b/internal/api/activitypub/users/repliesget.go
@@ -91,15 +91,15 @@ import (
// description: not found
func (m *Module) StatusRepliesGETHandler(c *gin.Context) {
// usernames on our instance are always lowercase
- requestedUsername := strings.ToLower(c.Param(UsernameKey))
- if requestedUsername == "" {
+ requestedUser := strings.ToLower(c.Param(apiutil.UsernameKey))
+ if requestedUser == "" {
err := errors.New("no username specified in request")
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
return
}
// status IDs on our instance are always uppercase
- requestedStatusID := strings.ToUpper(c.Param(StatusIDKey))
+ requestedStatusID := strings.ToUpper(c.Param(apiutil.IDKey))
if requestedStatusID == "" {
err := errors.New("no status id specified in request")
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
@@ -114,7 +114,7 @@ func (m *Module) StatusRepliesGETHandler(c *gin.Context) {
if contentType == string(apiutil.TextHTML) {
// redirect to the status
- c.Redirect(http.StatusSeeOther, "/@"+requestedUsername+"/statuses/"+requestedStatusID)
+ c.Redirect(http.StatusSeeOther, "/@"+requestedUser+"/statuses/"+requestedStatusID)
return
}
@@ -150,7 +150,7 @@ func (m *Module) StatusRepliesGETHandler(c *gin.Context) {
// Fetch serialized status replies response for input status.
resp, errWithCode := m.processor.Fedi().StatusRepliesGet(
c.Request.Context(),
- requestedUsername,
+ requestedUser,
requestedStatusID,
page,
onlyOtherAccounts,
diff --git a/internal/api/activitypub/users/repliesget_test.go b/internal/api/activitypub/users/repliesget_test.go
index 5d3754a8c..f50424626 100644
--- a/internal/api/activitypub/users/repliesget_test.go
+++ b/internal/api/activitypub/users/repliesget_test.go
@@ -28,7 +28,7 @@ import (
"code.superseriousbusiness.org/activity/streams"
"code.superseriousbusiness.org/activity/streams/vocab"
"code.superseriousbusiness.org/gotosocial/internal/ap"
- "code.superseriousbusiness.org/gotosocial/internal/api/activitypub/users"
+ apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
"code.superseriousbusiness.org/gotosocial/testrig"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
@@ -61,11 +61,11 @@ func (suite *RepliesGetTestSuite) TestGetReplies() {
// but because we're calling the function directly, we need to set them manually.
ctx.Params = gin.Params{
gin.Param{
- Key: users.UsernameKey,
+ Key: apiutil.UsernameKey,
Value: targetAccount.Username,
},
gin.Param{
- Key: users.StatusIDKey,
+ Key: apiutil.IDKey,
Value: targetStatus.ID,
},
}
@@ -96,7 +96,7 @@ func (suite *RepliesGetTestSuite) TestGetReplies() {
})
assert.Equal(suite.T(), expect, string(b))
- m := make(map[string]interface{})
+ m := make(map[string]any)
err = json.Unmarshal(b, &m)
assert.NoError(suite.T(), err)
@@ -129,11 +129,11 @@ func (suite *RepliesGetTestSuite) TestGetRepliesNext() {
// but because we're calling the function directly, we need to set them manually.
ctx.Params = gin.Params{
gin.Param{
- Key: users.UsernameKey,
+ Key: apiutil.UsernameKey,
Value: targetAccount.Username,
},
gin.Param{
- Key: users.StatusIDKey,
+ Key: apiutil.IDKey,
Value: targetStatus.ID,
},
}
@@ -167,7 +167,7 @@ func (suite *RepliesGetTestSuite) TestGetRepliesNext() {
})
assert.Equal(suite.T(), expect, string(b))
- m := make(map[string]interface{})
+ m := make(map[string]any)
err = json.Unmarshal(b, &m)
assert.NoError(suite.T(), err)
@@ -202,11 +202,11 @@ func (suite *RepliesGetTestSuite) TestGetRepliesLast() {
// but because we're calling the function directly, we need to set them manually.
ctx.Params = gin.Params{
gin.Param{
- Key: users.UsernameKey,
+ Key: apiutil.UsernameKey,
Value: targetAccount.Username,
},
gin.Param{
- Key: users.StatusIDKey,
+ Key: apiutil.IDKey,
Value: targetStatus.ID,
},
}
@@ -238,7 +238,7 @@ func (suite *RepliesGetTestSuite) TestGetRepliesLast() {
})
assert.Equal(suite.T(), expect, string(b))
- m := make(map[string]interface{})
+ m := make(map[string]any)
err = json.Unmarshal(b, &m)
assert.NoError(suite.T(), err)
diff --git a/internal/api/activitypub/users/statusget.go b/internal/api/activitypub/users/statusget.go
index 45b7847bf..843c3c2ff 100644
--- a/internal/api/activitypub/users/statusget.go
+++ b/internal/api/activitypub/users/statusget.go
@@ -30,15 +30,15 @@ import (
// StatusGETHandler serves the target status as an activitystreams NOTE so that other AP servers can parse it.
func (m *Module) StatusGETHandler(c *gin.Context) {
// usernames on our instance are always lowercase
- requestedUsername := strings.ToLower(c.Param(UsernameKey))
- if requestedUsername == "" {
+ requestedUser := strings.ToLower(c.Param(apiutil.UsernameKey))
+ if requestedUser == "" {
err := errors.New("no username specified in request")
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
return
}
// status IDs on our instance are always uppercase
- requestedStatusID := strings.ToUpper(c.Param(StatusIDKey))
+ requestedStatusID := strings.ToUpper(c.Param(apiutil.IDKey))
if requestedStatusID == "" {
err := errors.New("no status id specified in request")
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
@@ -53,11 +53,11 @@ func (m *Module) StatusGETHandler(c *gin.Context) {
if contentType == string(apiutil.TextHTML) {
// redirect to the status
- c.Redirect(http.StatusSeeOther, "/@"+requestedUsername+"/statuses/"+requestedStatusID)
+ c.Redirect(http.StatusSeeOther, "/@"+requestedUser+"/statuses/"+requestedStatusID)
return
}
- resp, errWithCode := m.processor.Fedi().StatusGet(c.Request.Context(), requestedUsername, requestedStatusID)
+ resp, errWithCode := m.processor.Fedi().StatusGet(c.Request.Context(), requestedUser, requestedStatusID)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
diff --git a/internal/api/activitypub/users/statusget_test.go b/internal/api/activitypub/users/statusget_test.go
index 275acdc02..99386c5bb 100644
--- a/internal/api/activitypub/users/statusget_test.go
+++ b/internal/api/activitypub/users/statusget_test.go
@@ -27,7 +27,7 @@ import (
"code.superseriousbusiness.org/activity/streams"
"code.superseriousbusiness.org/activity/streams/vocab"
- "code.superseriousbusiness.org/gotosocial/internal/api/activitypub/users"
+ apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
"code.superseriousbusiness.org/gotosocial/testrig"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/suite"
@@ -59,11 +59,11 @@ func (suite *StatusGetTestSuite) TestGetStatus() {
// but because we're calling the function directly, we need to set them manually.
ctx.Params = gin.Params{
gin.Param{
- Key: users.UsernameKey,
+ Key: apiutil.UsernameKey,
Value: targetAccount.Username,
},
gin.Param{
- Key: users.StatusIDKey,
+ Key: apiutil.IDKey,
Value: targetStatus.ID,
},
}
@@ -80,7 +80,7 @@ func (suite *StatusGetTestSuite) TestGetStatus() {
suite.NoError(err)
// should be a Note
- m := make(map[string]interface{})
+ m := make(map[string]any)
err = json.Unmarshal(b, &m)
suite.NoError(err)
@@ -118,11 +118,11 @@ func (suite *StatusGetTestSuite) TestGetStatusLowercase() {
// but because we're calling the function directly, we need to set them manually.
ctx.Params = gin.Params{
gin.Param{
- Key: users.UsernameKey,
+ Key: apiutil.UsernameKey,
Value: strings.ToLower(targetAccount.Username),
},
gin.Param{
- Key: users.StatusIDKey,
+ Key: apiutil.IDKey,
Value: strings.ToLower(targetStatus.ID),
},
}
@@ -139,7 +139,7 @@ func (suite *StatusGetTestSuite) TestGetStatusLowercase() {
suite.NoError(err)
// should be a Note
- m := make(map[string]interface{})
+ m := make(map[string]any)
err = json.Unmarshal(b, &m)
suite.NoError(err)
diff --git a/internal/api/activitypub/users/user.go b/internal/api/activitypub/users/user.go
index 7fb5f6aa5..e55608fe2 100644
--- a/internal/api/activitypub/users/user.go
+++ b/internal/api/activitypub/users/user.go
@@ -27,39 +27,17 @@ import (
)
const (
- // UsernameKey is for account usernames.
- UsernameKey = "username"
- // StatusIDKey is for status IDs
- StatusIDKey = "status"
- // OnlyOtherAccountsKey is for filtering status responses.
- OnlyOtherAccountsKey = "only_other_accounts"
- // MinIDKey is for filtering status responses.
- MinIDKey = "min_id"
- // MaxIDKey is for filtering status responses.
- MaxIDKey = "max_id"
- // PageKey is for filtering status responses.
- PageKey = "page"
-
- // BasePath is the base path for serving AP 'users' requests, minus the 'users' prefix.
- BasePath = "/:" + UsernameKey
- // InboxPath is for serving POST requests to a user's inbox with the given username key.
- InboxPath = BasePath + "/" + uris.InboxPath
- // OutboxPath is for serving GET requests to a user's outbox with the given username key.
- OutboxPath = BasePath + "/" + uris.OutboxPath
- // FollowersPath is for serving GET request's to a user's followers list, with the given username key.
- FollowersPath = BasePath + "/" + uris.FollowersPath
- // FollowingPath is for serving GET request's to a user's following list, with the given username key.
- FollowingPath = BasePath + "/" + uris.FollowingPath
- // FeaturedCollectionPath is for serving GET requests to a user's list of featured (pinned) statuses.
+ OnlyOtherAccountsKey = "only_other_accounts"
+ BasePath = "/:" + apiutil.UsernameKey
+ InboxPath = BasePath + "/" + uris.InboxPath
+ OutboxPath = BasePath + "/" + uris.OutboxPath
+ FollowersPath = BasePath + "/" + uris.FollowersPath
+ FollowingPath = BasePath + "/" + uris.FollowingPath
FeaturedCollectionPath = BasePath + "/" + uris.CollectionsPath + "/" + uris.FeaturedPath
- // StatusPath is for serving GET requests to a particular status by a user, with the given username key and status ID
- StatusPath = BasePath + "/" + uris.StatusesPath + "/:" + StatusIDKey
- // StatusRepliesPath is for serving the replies collection of a status.
- StatusRepliesPath = StatusPath + "/replies"
- // AcceptPath is for serving accepts of a status.
- AcceptPath = BasePath + "/" + uris.AcceptsPath + "/:" + apiutil.IDKey
- // AuthorizationsPath is for serving authorizations of an interaction.
- AuthorizationsPath = BasePath + "/" + uris.AuthorizationsPath + "/:" + apiutil.IDKey
+ StatusPath = BasePath + "/" + uris.StatusesPath + "/:" + apiutil.IDKey
+ StatusRepliesPath = StatusPath + "/replies"
+ AcceptPath = BasePath + "/" + uris.AcceptsPath + "/:" + apiutil.IDKey
+ AuthorizationsPath = BasePath + "/" + uris.AuthorizationsPath + "/:" + apiutil.IDKey
)
type Module struct {
diff --git a/internal/api/activitypub/users/userget.go b/internal/api/activitypub/users/userget.go
index 00d8a0f1f..9841e08d6 100644
--- a/internal/api/activitypub/users/userget.go
+++ b/internal/api/activitypub/users/userget.go
@@ -38,8 +38,8 @@ import (
// request is blocked.
func (m *Module) UsersGETHandler(c *gin.Context) {
// usernames on our instance are always lowercase
- requestedUsername := strings.ToLower(c.Param(UsernameKey))
- if requestedUsername == "" {
+ requestedUser := strings.ToLower(c.Param(apiutil.UsernameKey))
+ if requestedUser == "" {
err := errors.New("no username specified in request")
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
return
@@ -51,13 +51,17 @@ func (m *Module) UsersGETHandler(c *gin.Context) {
return
}
+ // If HTML is requested, redirect
+ // to user's profile instead.
if contentType == string(apiutil.TextHTML) {
- // redirect to the user's profile
- c.Redirect(http.StatusSeeOther, "/@"+requestedUsername)
+ c.Redirect(http.StatusSeeOther, "/@"+requestedUser)
return
}
- resp, errWithCode := m.processor.Fedi().UserGet(c.Request.Context(), requestedUsername, c.Request.URL)
+ resp, errWithCode := m.processor.Fedi().UserGet(
+ c.Request.Context(),
+ requestedUser,
+ )
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
diff --git a/internal/api/activitypub/users/userget_test.go b/internal/api/activitypub/users/userget_test.go
index a9cba468f..648687d6e 100644
--- a/internal/api/activitypub/users/userget_test.go
+++ b/internal/api/activitypub/users/userget_test.go
@@ -27,6 +27,7 @@ import (
"code.superseriousbusiness.org/activity/streams"
"code.superseriousbusiness.org/activity/streams/vocab"
"code.superseriousbusiness.org/gotosocial/internal/api/activitypub/users"
+ apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
"code.superseriousbusiness.org/gotosocial/testrig"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/suite"
@@ -57,7 +58,7 @@ func (suite *UserGetTestSuite) TestGetUser() {
// but because we're calling the function directly, we need to set them manually.
ctx.Params = gin.Params{
gin.Param{
- Key: users.UsernameKey,
+ Key: apiutil.UsernameKey,
Value: targetAccount.Username,
},
}
@@ -74,7 +75,7 @@ func (suite *UserGetTestSuite) TestGetUser() {
suite.NoError(err)
// should be a Person
- m := make(map[string]interface{})
+ m := make(map[string]any)
err = json.Unmarshal(b, &m)
suite.NoError(err)
@@ -125,7 +126,7 @@ func (suite *UserGetTestSuite) TestGetUserPublicKeyDeleted() {
// but because we're calling the function directly, we need to set them manually.
ctx.Params = gin.Params{
gin.Param{
- Key: users.UsernameKey,
+ Key: apiutil.UsernameKey,
Value: targetAccount.Username,
},
}
@@ -142,7 +143,7 @@ func (suite *UserGetTestSuite) TestGetUserPublicKeyDeleted() {
suite.NoError(err)
// should be a Person
- m := make(map[string]interface{})
+ m := make(map[string]any)
err = json.Unmarshal(b, &m)
suite.NoError(err)
diff --git a/internal/api/wellknown/webfinger/webfingerget.go b/internal/api/wellknown/webfinger/webfingerget.go
index e277622fd..7c8f1cf84 100644
--- a/internal/api/wellknown/webfinger/webfingerget.go
+++ b/internal/api/wellknown/webfinger/webfingerget.go
@@ -67,7 +67,7 @@ func (m *Module) WebfingerGETRequest(c *gin.Context) {
return
}
- requestedUsername, requestedHost, err := util.ExtractWebfingerParts(resourceQuery)
+ requestedUser, requestedHost, err := util.ExtractWebfingerParts(resourceQuery)
if err != nil {
err := fmt.Errorf("bad webfinger request with resource query %s: %w", resourceQuery, err)
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
@@ -80,7 +80,7 @@ func (m *Module) WebfingerGETRequest(c *gin.Context) {
return
}
- resp, errWithCode := m.processor.Fedi().WebfingerGet(c.Request.Context(), requestedUsername)
+ resp, errWithCode := m.processor.Fedi().WebfingerGet(c.Request.Context(), requestedUser)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
diff --git a/internal/federation/authenticate.go b/internal/federation/authenticate.go
index f8515e649..fabef822c 100644
--- a/internal/federation/authenticate.go
+++ b/internal/federation/authenticate.go
@@ -36,6 +36,7 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/gtscontext"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/id"
"code.superseriousbusiness.org/gotosocial/internal/log"
"code.superseriousbusiness.org/httpsig"
"codeberg.org/gruf/go-kv/v2"
@@ -103,7 +104,10 @@ type PubKeyAuth struct {
//
// Note that it is also valid to pass in an empty string here, in which case the
// keys of the instance account will be used.
-func (f *Federator) AuthenticateFederatedRequest(ctx context.Context, requestedUsername string) (*PubKeyAuth, gtserror.WithCode) {
+//
+// The caller of this function MUST CHECK AT SOME POINT WHETHER THE PUB KEY OWNER
+// HAS BEEN SUSPENDED, and handle it appropriately, as this function will not do so!
+func (f *Federator) AuthenticateFederatedRequest(ctx context.Context, requestedUser string) (*PubKeyAuth, gtserror.WithCode) {
// Thanks to the signature check middleware,
// we should already have an http signature
// verifier set on the context. If we don't,
@@ -144,7 +148,7 @@ func (f *Federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
l := log.
WithContext(ctx).
WithFields(kv.Fields{
- {"requestedUsername", requestedUsername},
+ {"requestedUser", requestedUser},
{"pubKeyID", pubKeyIDStr},
}...)
@@ -153,7 +157,7 @@ func (f *Federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
pubKeyAuth, errWithCode = f.derefPubKeyDBOnly(ctx, pubKeyIDStr)
} else {
l.Trace("public key is remote, checking if we need to dereference")
- pubKeyAuth, errWithCode = f.derefPubKey(ctx, requestedUsername, pubKeyIDStr, pubKeyID)
+ pubKeyAuth, errWithCode = f.derefPubKey(ctx, requestedUser, pubKeyIDStr, pubKeyID)
}
if errWithCode != nil {
@@ -182,7 +186,7 @@ func (f *Federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
// Ensure we have instance stored in
// database for the account at URI.
err := f.fetchAccountInstance(ctx,
- requestedUsername,
+ requestedUser,
pubKeyAuth.OwnerURI,
)
if err != nil {
@@ -192,7 +196,7 @@ func (f *Federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
// If we're currently handshaking with another instance, return
// without derefing the owner, the only possible time we do this.
// This prevents deadlocks when GTS instances mutually deref.
- if f.Handshaking(requestedUsername, pubKeyAuth.OwnerURI) {
+ if f.Handshaking(requestedUser, pubKeyAuth.OwnerURI) {
log.Warnf(ctx, "network race during %s handshake", pubKeyAuth.OwnerURI)
pubKeyAuth.Handshaking = true
return pubKeyAuth, nil
@@ -201,22 +205,14 @@ func (f *Federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
// Dereference the account located at owner URI.
// Use exact URI match, not URL match.
pubKeyAuth.Owner, _, err = f.GetAccountByURI(ctx,
- requestedUsername,
+ requestedUser,
pubKeyAuth.OwnerURI,
false,
)
if err != nil {
- if gtserror.StatusCode(err) == http.StatusGone {
- // This can happen here instead of the pubkey 'gone'
- // checks due to: the server sending account deletion
- // notifications out, we start processing, the request above
- // succeeds, and *then* the profile is removed and starts
- // returning 410 Gone, at which point _this_ request fails.
- return nil, gtserror.NewErrorGone(err)
- }
-
- err := gtserror.Newf("error dereferencing account %s: %w", pubKeyAuth.OwnerURI, err)
- return nil, gtserror.NewErrorInternalError(err)
+ // If we couldn't fetch the pub key owner either from
+ // the DB or remote, return appropriate error code.
+ return nil, keyOwnerFetchError(err, pubKeyAuth.OwnerURI)
}
// Catch a possible (but very rare) race condition where
@@ -231,12 +227,68 @@ func (f *Federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
}
}
- if !pubKeyAuth.Owner.SuspendedAt.IsZero() {
- const text = "requesting account suspended"
- return nil, gtserror.NewErrorForbidden(errors.New(text))
+ return pubKeyAuth, nil
+}
+
+// keyOwnerFetchError checks the given error for the
+// attempt to fetch the given account URI, and wraps
+// the error appropriately based on what went wrong.
+func keyOwnerFetchError(
+ err error,
+ acctURI *url.URL,
+) gtserror.WithCode {
+ var newErr error
+
+ // Check if a status code was returned
+ // from the failed dereference attempt.
+ switch statusCode := gtserror.StatusCode(err); statusCode {
+
+ case http.StatusUnauthorized:
+ // If we got 401 Unauthorized from the remote,
+ // something likely went wrong with signature
+ // verification. In this case we should also
+ // return unauthorized, as we can't validate.
+ //
+ // Unlike with forbidden, we should warn log
+ // about this, as it may indicate some kind
+ // of key mismatch necessitating admin action.
+ newErr = gtserror.Newf(
+ "received 401 Unauthorized fetching pub key owner %s: %w",
+ acctURI, err,
+ )
+
+ case http.StatusForbidden:
+ // If we got 403 Forbidden from the remote,
+ // we're not allowed to see the account making
+ // the request. In this case we should just
+ // return unauthorized, as we can't validate.
+ newErr = gtserror.Newf(
+ "received 403 Forbidden fetching pub key owner %s: %w",
+ acctURI, err,
+ )
+
+ case http.StatusGone:
+ // This can happen here instead of the pubkey
+ // 'gone' checks due to: the server sending account
+ // deletion notifications out, we start processing,
+ // the request above succeeds, and *then* the profile
+ // is removed and starts returning 410 Gone, at
+ // which point _this_ request fails.
+ newErr = gtserror.Newf(
+ "received 410 Gone fetching pub key owner %s: %w",
+ acctURI, err,
+ )
+
+ default:
+ // Handle all other errors
+ // (server not available etc).
+ newErr = gtserror.Newf(
+ "could not dereference pub key owner %s: %w",
+ acctURI, err,
+ )
}
- return pubKeyAuth, nil
+ return gtserror.NewErrorUnauthorized(newErr)
}
// derefPubKeyDBOnly tries to dereference the given
@@ -286,7 +338,7 @@ func (f *Federator) derefPubKeyDBOnly(
// extracting the key.
func (f *Federator) derefPubKey(
ctx context.Context,
- requestedUsername string,
+ requestedUser string,
pubKeyIDStr string,
pubKeyID *url.URL,
) (
@@ -296,10 +348,27 @@ func (f *Federator) derefPubKey(
l := log.
WithContext(ctx).
WithFields(kv.Fields{
- {"requestedUsername", requestedUsername},
+ {"requestedUser", requestedUser},
{"pubKeyID", pubKeyIDStr},
}...)
+ // If we've tried to get this pub key before and we
+ // now have a tombstone for it (ie., it's been deleted
+ // from remote), don't try to dereference it again.
+ gone, err := f.db.TombstoneExistsWithURI(ctx, pubKeyIDStr)
+ if err != nil {
+ err := gtserror.Newf("error checking for tombstone (%s): %w", pubKeyIDStr, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if gone {
+ // If we had a tombstone for the remote, set an HTTP
+ // response code of 410 Gone on the returned errWithCode.
+ err := gtserror.Newf("account with public key %s is gone: %w", pubKeyID, err)
+ err = gtserror.WithStatusCode(err, http.StatusGone)
+ return nil, gtserror.NewErrorUnauthorized(err)
+ }
+
// Try a database only deref first. We may already
// have the requesting account cached locally.
pubKeyAuth, errWithCode := f.derefPubKeyDBOnly(ctx, pubKeyIDStr)
@@ -332,22 +401,8 @@ func (f *Federator) derefPubKey(
)
}
- // If we've tried to get this account before and we
- // now have a tombstone for it (ie., it's been deleted
- // from remote), don't try to dereference it again.
- gone, err := f.CheckGone(ctx, pubKeyID)
- if err != nil {
- err := gtserror.Newf("error checking for tombstone (%s): %w", pubKeyIDStr, err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- if gone {
- err := gtserror.Newf("account with public key is gone (%s)", pubKeyIDStr)
- return nil, gtserror.NewErrorGone(err)
- }
-
// Make an http call to get the (refreshed) pubkey.
- pubKeyBytes, errWithCode := f.callForPubKey(ctx, requestedUsername, pubKeyID)
+ pubKeyBytes, errWithCode := f.callForPubKey(ctx, requestedUser, pubKeyID)
if errWithCode != nil {
return nil, errWithCode
}
@@ -399,10 +454,10 @@ func (f *Federator) derefPubKey(
// callForPubKey handles the nitty gritty of actually
// making a request for the given pubKeyID with a
-// transport created on behalf of requestedUsername.
+// transport created on behalf of requestedUser.
func (f *Federator) callForPubKey(
ctx context.Context,
- requestedUsername string,
+ requestedUser string,
pubKeyID *url.URL,
) ([]byte, gtserror.WithCode) {
// Use a transport to dereference the remote.
@@ -410,10 +465,10 @@ func (f *Federator) callForPubKey(
// We're on a hot path: don't retry if req fails.
gtscontext.SetFastFail(ctx),
- requestedUsername,
+ requestedUser,
)
if err != nil {
- err = gtserror.Newf("error creating transport for %s: %w", requestedUsername, err)
+ err = gtserror.Newf("error creating transport for %s: %w", requestedUser, err)
return nil, gtserror.NewErrorInternalError(err)
}
@@ -439,13 +494,21 @@ func (f *Federator) callForPubKey(
// (account deleted, moved, etc). Add a tombstone
// to our database so that we can avoid trying to
// dereference it in future.
- if err := f.HandleGone(ctx, pubKeyID); err != nil {
- err := gtserror.Newf("error marking public key %s as gone: %w", pubKeyID, err)
+ pubKeyIDStr := pubKeyID.String()
+ tombstone := &gtsmodel.Tombstone{
+ ID: id.NewULID(),
+ Domain: pubKeyID.Host,
+ URI: pubKeyIDStr,
+ }
+ if err := f.db.PutTombstone(ctx, tombstone); err != nil && !errors.Is(err, db.ErrAlreadyExists) {
+ err := gtserror.Newf("db error adding tombstone for pub key %s: %w", pubKeyIDStr, err)
return nil, gtserror.NewErrorInternalError(err)
}
- err := gtserror.Newf("account with public key %s is gone", pubKeyID)
- return nil, gtserror.NewErrorGone(err)
+ // Wrap the error to preserve the 410 Gone status code.
+ err := gtserror.Newf("account with public key %s is gone: %w", pubKeyID, err)
+ err = gtserror.WithStatusCode(err, http.StatusGone)
+ return nil, gtserror.NewErrorUnauthorized(err)
}
// Fall back to generic error.
@@ -457,7 +520,7 @@ func (f *Federator) callForPubKey(
// the database for the given account URI, deref'ing if necessary.
func (f *Federator) fetchAccountInstance(
ctx context.Context,
- requestedUsername string,
+ requestedUser string,
accountURI *url.URL,
) error {
// Look for an existing entry for instance in database.
@@ -475,7 +538,7 @@ func (f *Federator) fetchAccountInstance(
// instance yet; go dereference it.
instance, err = f.GetRemoteInstance(
gtscontext.SetFastFail(ctx),
- requestedUsername,
+ requestedUser,
&url.URL{
Scheme: accountURI.Scheme,
Host: accountURI.Host,
diff --git a/internal/federation/federatingactor.go b/internal/federation/federatingactor.go
index 2a68865aa..316835036 100644
--- a/internal/federation/federatingactor.go
+++ b/internal/federation/federatingactor.go
@@ -31,6 +31,7 @@ import (
apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
"code.superseriousbusiness.org/gotosocial/internal/config"
"code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/log"
"code.superseriousbusiness.org/gotosocial/internal/uris"
@@ -162,6 +163,31 @@ func (f *federatingActor) PostInboxScheme(ctx context.Context, w http.ResponseWr
return false, gtserror.NewErrorUnauthorized(errors.New(text), text)
}
+ // Ensure requester is not suspended.
+ requester := gtscontext.RequestingAccount(ctx)
+ switch {
+ case !requester.IsSuspended():
+ // Account in good standing.
+ // Allow request to continue.
+
+ case requester.DeletedSelf():
+ // Looks like pub key owner deleted their own account.
+ // Likely their instance is still sending out deletes,
+ // but we'll have already deleted everything of theirs.
+ // Don't do any further processing of the request.
+ log.Debugf(ctx,
+ "requesting account %s self deleted, ignoring inbox post",
+ requester.UsernameDomain(),
+ )
+ return true, nil
+
+ default:
+ // Likely suspended by an admin or
+ // defed action on *this* instance.
+ const text = "requesting account suspended"
+ return false, gtserror.NewErrorForbidden(errors.New(text), text)
+ }
+
/*
Begin processing the request, but note that we
have not yet applied authorization (ie., blocks).
diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go
index e1ec86b32..2f6953257 100644
--- a/internal/federation/federatingprotocol.go
+++ b/internal/federation/federatingprotocol.go
@@ -198,39 +198,56 @@ func (f *Federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr
// account by parsing username from `/users/{username}/inbox`.
username, err := uris.ParseInboxPath(r.URL)
if err != nil {
- err = gtserror.Newf("could not parse %s as inbox path: %w", r.URL.String(), err)
+ err := gtserror.Newf("could not parse %s as inbox path: %w", r.URL.String(), err)
return nil, false, err
}
if username == "" {
- err = gtserror.New("inbox username was empty")
+ err := gtserror.New("inbox username was empty")
return nil, false, err
}
- receivingAccount, err := f.db.GetAccountByUsernameDomain(ctx, username, "")
- if err != nil {
- err = gtserror.Newf("could not fetch receiving account %s: %w", username, err)
+ // Get the receiving local account inbox
+ // owner with given username from database.
+ receiver, err := f.db.GetAccountByUsernameDomain(ctx, username, "")
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting receiving account %s: %w", username, err)
return nil, false, err
}
+ if receiver == nil {
+ // Maybe we had this account at some point and someone
+ // manually deleted it from the DB. Just return not found.
+ err := gtserror.Newf("receiving account %s not found in the db", username)
+ errWithCode := gtserror.NewErrorNotFound(err)
+ return ctx, false, errWithCode
+ }
+
// Check who's trying to deliver to us by inspecting the http signature.
- pubKeyAuth, errWithCode := f.AuthenticateFederatedRequest(ctx, receivingAccount.Username)
+ pubKeyAuth, errWithCode := f.AuthenticateFederatedRequest(ctx, receiver.Username)
if errWithCode != nil {
- switch errWithCode.Code() {
- case http.StatusUnauthorized, http.StatusForbidden, http.StatusBadRequest:
- // If codes 400, 401, or 403, obey the go-fed
- // interface by writing the header and bailing.
- w.WriteHeader(errWithCode.Code())
- case http.StatusGone:
- // If the requesting account's key has gone
- // (410) then likely inbox post was a delete.
+
+ // Check if we got an error code from a remote
+ // instance while trying to dereference the pub
+ // key owner who's trying to post to this inbox.
+ if gtserror.StatusCode(errWithCode) == http.StatusGone {
+ // If the pub key owner's key/account has gone
+ // (410) then likely inbox post was a Delete.
//
- // We can just write 202 and leave: we didn't
- // know about the account anyway, so we can't
- // do any further processing.
+ // If so, we can just write 202 and leave, as
+ // either we'll have already processed any Deletes
+ // sent by this account, or we never met the account
+ // in the first place so we don't have any of their
+ // stuff stored to actually delete.
w.WriteHeader(http.StatusAccepted)
+ return ctx, false, nil
}
+ // In all other cases, obey the go-fed
+ // interface by writing the status
+ // code from the returned ErrWithCode.
+ w.WriteHeader(errWithCode.Code())
+
// We still return the error
// for later request logging.
return ctx, false, errWithCode
@@ -247,7 +264,11 @@ func (f *Federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr
// We have everything we need now, set the requesting
// and receiving accounts on the context for later use.
ctx = gtscontext.SetRequestingAccount(ctx, pubKeyAuth.Owner)
- ctx = gtscontext.SetReceivingAccount(ctx, receivingAccount)
+ ctx = gtscontext.SetReceivingAccount(ctx, receiver)
+
+ // Note: we do not check here yet whether requesting
+ // account has been suspended or self-deleted, as that
+ // is handled in *federatingActor.PostInboxScheme
return ctx, true, nil
}
@@ -290,7 +311,7 @@ func (f *Federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er
// then we can save some work.
blocked, err := f.db.AreURIsBlocked(ctx, actorIRIs)
if err != nil {
- err = gtserror.Newf("error checking domain blocks of actorIRIs: %w", err)
+ err := gtserror.Newf("error checking domain blocks of actorIRIs: %w", err)
return false, err
}
@@ -302,7 +323,7 @@ func (f *Federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er
// Now user level blocks. Receiver should not block requester.
blocked, err = f.db.IsBlocked(ctx, receivingAccount.ID, requestingAccount.ID)
if err != nil {
- err = gtserror.Newf("db error checking block between receiver and requester: %w", err)
+ err := gtserror.Newf("db error checking block between receiver and requester: %w", err)
return false, err
}
@@ -358,7 +379,7 @@ func (f *Federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er
)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
// Real db error.
- err = gtserror.Newf("db error trying to get %s as account: %w", iriStr, err)
+ err := gtserror.Newf("db error trying to get %s as account: %w", iriStr, err)
return false, err
} else if err == nil {
// IRI is for an account.
@@ -373,7 +394,7 @@ func (f *Federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er
)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
// Real db error.
- err = gtserror.Newf("db error trying to get %s as status: %w", iriStr, err)
+ err := gtserror.Newf("db error trying to get %s as status: %w", iriStr, err)
return false, err
} else if err == nil {
// IRI is for a status.
@@ -395,9 +416,9 @@ func (f *Federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er
// account they have blocked. In this case, it's v. unlikely
// they care to see the boost in their timeline, so there's
// no point in us processing it.
- blocked, err = f.db.IsBlocked(ctx, receivingAccount.ID, accountID)
+ blocked, err := f.db.IsBlocked(ctx, receivingAccount.ID, accountID)
if err != nil {
- err = gtserror.Newf("db error checking block between receiver and other account: %w", err)
+ err := gtserror.Newf("db error checking block between receiver and other account: %w", err)
return false, err
}
@@ -418,9 +439,9 @@ func (f *Federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er
// accounts are gossiping about + trying to tag a third account
// who has one or the other of them blocked.
if iriHost == ourHost {
- blocked, err = f.db.IsBlocked(ctx, accountID, requestingAccount.ID)
+ blocked, err := f.db.IsBlocked(ctx, accountID, requestingAccount.ID)
if err != nil {
- err = gtserror.Newf("db error checking block between other account and requester: %w", err)
+ err := gtserror.Newf("db error checking block between other account and requester: %w", err)
return false, err
}
diff --git a/internal/federation/gone.go b/internal/federation/gone.go
deleted file mode 100644
index 932bd17a1..000000000
--- a/internal/federation/gone.go
+++ /dev/null
@@ -1,41 +0,0 @@
-// GoToSocial
-// Copyright (C) GoToSocial Authors admin@gotosocial.org
-// SPDX-License-Identifier: AGPL-3.0-or-later
-//
-// 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 federation
-
-import (
- "context"
- "net/url"
-
- "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
- "code.superseriousbusiness.org/gotosocial/internal/id"
-)
-
-// CheckGone checks if a tombstone exists in the database for AP Actor or Object with the given uri.
-func (f *Federator) CheckGone(ctx context.Context, uri *url.URL) (bool, error) {
- return f.db.TombstoneExistsWithURI(ctx, uri.String())
-}
-
-// HandleGone puts a tombstone in the database, which marks an AP Actor or Object with the given uri as gone.
-func (f *Federator) HandleGone(ctx context.Context, uri *url.URL) error {
- tombstone := &gtsmodel.Tombstone{
- ID: id.NewULID(),
- Domain: uri.Host,
- URI: uri.String(),
- }
- return f.db.PutTombstone(ctx, tombstone)
-}
diff --git a/internal/gtsmodel/account.go b/internal/gtsmodel/account.go
index 8b2de6b23..bd4fe1074 100644
--- a/internal/gtsmodel/account.go
+++ b/internal/gtsmodel/account.go
@@ -392,6 +392,12 @@ func (a *Account) IsSuspended() bool {
return !a.SuspendedAt.IsZero()
}
+// DeletedSelf returns true
+// if account deleted itself.
+func (a *Account) DeletedSelf() bool {
+ return a.SuspensionOrigin == a.ID
+}
+
// IsMoving returns true if
// account is Moving or has Moved.
func (a *Account) IsMoving() bool {
diff --git a/internal/processing/fedi/accept.go b/internal/processing/fedi/accept.go
deleted file mode 100644
index 97e36fbb3..000000000
--- a/internal/processing/fedi/accept.go
+++ /dev/null
@@ -1,57 +0,0 @@
-// GoToSocial
-// Copyright (C) GoToSocial Authors admin@gotosocial.org
-// SPDX-License-Identifier: AGPL-3.0-or-later
-//
-// 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 fedi
-
-import (
- "context"
-
- "code.superseriousbusiness.org/gotosocial/internal/ap"
- "code.superseriousbusiness.org/gotosocial/internal/gtserror"
-)
-
-// AcceptGet handles the getting of a fedi/activitypub
-// representation of a local interaction acceptance.
-//
-// It performs appropriate authentication before
-// returning a JSON serializable interface.
-func (p *Processor) AcceptGet(
- ctx context.Context,
- requestedUser string,
- intReqID string,
-) (any, gtserror.WithCode) {
- // Ensure valid request, intReq exists, etc.
- intReq, errWithCode := p.validateIntReqRequest(ctx, requestedUser, intReqID)
- if errWithCode != nil {
- return nil, errWithCode
- }
-
- // Convert + serialize the Accept.
- accept, err := p.converter.InteractionReqToASAccept(ctx, intReq)
- if err != nil {
- err := gtserror.Newf("error converting to accept: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- data, err := ap.Serialize(accept)
- if err != nil {
- err := gtserror.Newf("error serializing accept: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- return data, nil
-}
diff --git a/internal/processing/fedi/authorization.go b/internal/processing/fedi/authorization.go
index bbba6a2d8..276dd3a82 100644
--- a/internal/processing/fedi/authorization.go
+++ b/internal/processing/fedi/authorization.go
@@ -19,9 +19,13 @@ package fedi
import (
"context"
+ "errors"
+ "fmt"
"code.superseriousbusiness.org/gotosocial/internal/ap"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
// AuthorizationGet handles the getting of a fedi/activitypub
@@ -35,7 +39,7 @@ func (p *Processor) AuthorizationGet(
intReqID string,
) (any, gtserror.WithCode) {
// Ensure valid request, intReq exists, etc.
- intReq, errWithCode := p.validateIntReqRequest(ctx, requestedUser, intReqID)
+ intReq, errWithCode := p.validateAuthGetRequest(ctx, requestedUser, intReqID)
if errWithCode != nil {
return nil, errWithCode
}
@@ -55,3 +59,84 @@ func (p *Processor) AuthorizationGet(
return data, nil
}
+
+// AcceptGet handles the getting of a fedi/activitypub
+// representation of a local interaction acceptance.
+//
+// It performs appropriate authentication before
+// returning a JSON serializable interface.
+func (p *Processor) AcceptGet(
+ ctx context.Context,
+ requestedUser string,
+ intReqID string,
+) (any, gtserror.WithCode) {
+ // Ensure valid request, intReq exists, etc.
+ intReq, errWithCode := p.validateAuthGetRequest(ctx, requestedUser, intReqID)
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ // Convert + serialize the Accept.
+ accept, err := p.converter.InteractionReqToASAccept(ctx, intReq)
+ if err != nil {
+ err := gtserror.Newf("error converting to accept: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ data, err := ap.Serialize(accept)
+ if err != nil {
+ err := gtserror.Newf("error serializing accept: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return data, nil
+}
+
+// validateAuthGetRequest is a shortcut function
+// for returning an accepted interaction request
+// targeting `requestedUser`.
+func (p *Processor) validateAuthGetRequest(
+ ctx context.Context,
+ requestedUser string,
+ intReqID string,
+) (*gtsmodel.InteractionRequest, gtserror.WithCode) {
+ // Authenticate incoming request, getting related accounts.
+ auth, errWithCode := p.authenticate(ctx, requestedUser)
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ if auth.handshakingURI != nil {
+ // We're currently handshaking, which means we don't know
+ // this account yet. This should be a very rare race condition.
+ err := gtserror.Newf("network race handshaking %s", auth.handshakingURI)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Fetch interaction request with the given ID.
+ req, err := p.state.DB.GetInteractionRequestByID(ctx, intReqID)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting interaction request %s: %w", intReqID, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Ensure that this is an existing
+ // and *accepted* interaction request.
+ if req == nil || !req.IsAccepted() {
+ const text = "interaction request not found"
+ return nil, gtserror.NewErrorNotFound(errors.New(text))
+ }
+
+ // Ensure interaction request was accepted
+ // by the account in the request path.
+ if req.TargetAccountID != auth.receiver.ID {
+ text := fmt.Sprintf(
+ "account %s is not targeted by interaction request %s and therefore can't accept it",
+ requestedUser, intReqID,
+ )
+ return nil, gtserror.NewErrorNotFound(errors.New(text))
+ }
+
+ // All fine.
+ return req, nil
+}
diff --git a/internal/processing/fedi/collections.go b/internal/processing/fedi/collections.go
index ae4860b15..b67651dff 100644
--- a/internal/processing/fedi/collections.go
+++ b/internal/processing/fedi/collections.go
@@ -54,24 +54,24 @@ func (p *Processor) OutboxGet(
ctx context.Context,
requestedUser string,
page *paging.Page,
-) (interface{}, gtserror.WithCode) {
+) (any, gtserror.WithCode) {
// Authenticate incoming request, getting related accounts.
auth, errWithCode := p.authenticate(ctx, requestedUser)
if errWithCode != nil {
return nil, errWithCode
}
- receivingAcct := auth.receivingAcct
+ receiver := auth.receiver
// Parse the collection ID object from account's followers URI.
- collectionID, err := url.Parse(receivingAcct.OutboxURI)
+ collectionID, err := url.Parse(receiver.OutboxURI)
if err != nil {
- err := gtserror.Newf("error parsing account outbox uri %s: %w", receivingAcct.OutboxURI, err)
+ err := gtserror.Newf("error parsing account outbox uri %s: %w", receiver.OutboxURI, err)
return nil, gtserror.NewErrorInternalError(err)
}
// Ensure we have stats for this account.
- if err := p.state.DB.PopulateAccountStats(ctx, receivingAcct); err != nil {
- err := gtserror.Newf("error getting stats for account %s: %w", receivingAcct.ID, err)
+ if err := p.state.DB.PopulateAccountStats(ctx, receiver); err != nil {
+ err := gtserror.Newf("error getting stats for account %s: %w", receiver.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
@@ -83,8 +83,8 @@ func (p *Processor) OutboxGet(
switch {
- case receivingAcct.IsInstance() ||
- *receivingAcct.Settings.HideCollections:
+ case receiver.IsInstance() ||
+ *receiver.Settings.HideCollections:
// If account that hides collections, or instance
// account (ie., can't post / have relationships),
// just return barest stub of collection.
@@ -94,7 +94,7 @@ func (p *Processor) OutboxGet(
// If paging disabled, or we're currently handshaking
// the requester, just return collection that links
// to first page (i.e. path below), with no items.
- params.Total = util.Ptr(*receivingAcct.Stats.StatusesCount)
+ params.Total = util.Ptr(*receiver.Stats.StatusesCount)
params.First = new(paging.Page)
params.Query = make(url.Values, 1)
params.Query.Set("limit", "40") // enables paging
@@ -105,7 +105,7 @@ func (p *Processor) OutboxGet(
// Get page of full public statuses.
statuses, err := p.state.DB.GetAccountStatuses(
ctx,
- receivingAcct.ID,
+ receiver.ID,
page.GetLimit(), // limit
true, // excludeReplies
true, // excludeReblogs
@@ -133,7 +133,7 @@ func (p *Processor) OutboxGet(
// (eg., local-only statuses, if the requester is remote).
statuses, err = p.visFilter.StatusesVisible(
ctx,
- auth.requestingAcct,
+ auth.requester,
statuses,
)
if err != nil {
@@ -142,7 +142,7 @@ func (p *Processor) OutboxGet(
}
// Start building AS collection page params.
- params.Total = util.Ptr(*receivingAcct.Stats.StatusesCount)
+ params.Total = util.Ptr(*receiver.Stats.StatusesCount)
var pageParams ap.CollectionPageParams
pageParams.CollectionParams = params
@@ -194,24 +194,24 @@ func (p *Processor) FollowersGet(
ctx context.Context,
requestedUser string,
page *paging.Page,
-) (interface{}, gtserror.WithCode) {
+) (any, gtserror.WithCode) {
// Authenticate incoming request, getting related accounts.
auth, errWithCode := p.authenticate(ctx, requestedUser)
if errWithCode != nil {
return nil, errWithCode
}
- receivingAcct := auth.receivingAcct
+ receiver := auth.receiver
// Parse the collection ID object from account's followers URI.
- collectionID, err := url.Parse(receivingAcct.FollowersURI)
+ collectionID, err := url.Parse(receiver.FollowersURI)
if err != nil {
- err := gtserror.Newf("error parsing account followers uri %s: %w", receivingAcct.FollowersURI, err)
+ err := gtserror.Newf("error parsing account followers uri %s: %w", receiver.FollowersURI, err)
return nil, gtserror.NewErrorInternalError(err)
}
// Ensure we have stats for this account.
- if err := p.state.DB.PopulateAccountStats(ctx, receivingAcct); err != nil {
- err := gtserror.Newf("error getting stats for account %s: %w", receivingAcct.ID, err)
+ if err := p.state.DB.PopulateAccountStats(ctx, receiver); err != nil {
+ err := gtserror.Newf("error getting stats for account %s: %w", receiver.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
@@ -223,8 +223,8 @@ func (p *Processor) FollowersGet(
switch {
- case receivingAcct.IsInstance() ||
- *receivingAcct.Settings.HideCollections:
+ case receiver.IsInstance() ||
+ *receiver.Settings.HideCollections:
// If account that hides collections, or instance
// account (ie., can't post / have relationships),
// just return barest stub of collection.
@@ -234,7 +234,7 @@ func (p *Processor) FollowersGet(
// If paging disabled, or we're currently handshaking
// the requester, just return collection that links
// to first page (i.e. path below), with no items.
- params.Total = util.Ptr(*receivingAcct.Stats.FollowersCount)
+ params.Total = util.Ptr(*receiver.Stats.FollowersCount)
params.First = new(paging.Page)
params.Query = make(url.Values, 1)
params.Query.Set("limit", "40") // enables paging
@@ -243,7 +243,7 @@ func (p *Processor) FollowersGet(
default:
// Paging enabled.
// Get page of full follower objects with attached accounts.
- followers, err := p.state.DB.GetAccountFollowers(ctx, receivingAcct.ID, page)
+ followers, err := p.state.DB.GetAccountFollowers(ctx, receiver.ID, page)
if err != nil {
err := gtserror.Newf("error getting followers: %w", err)
return nil, gtserror.NewErrorInternalError(err)
@@ -260,7 +260,7 @@ func (p *Processor) FollowersGet(
}
// Start building AS collection page params.
- params.Total = util.Ptr(*receivingAcct.Stats.FollowersCount)
+ params.Total = util.Ptr(*receiver.Stats.FollowersCount)
var pageParams ap.CollectionPageParams
pageParams.CollectionParams = params
@@ -306,24 +306,24 @@ func (p *Processor) FollowersGet(
// FollowingGet returns the serialized ActivityPub
// collection of a local account's following collection,
// which contains links to accounts followed by this account.
-func (p *Processor) FollowingGet(ctx context.Context, requestedUser string, page *paging.Page) (interface{}, gtserror.WithCode) {
+func (p *Processor) FollowingGet(ctx context.Context, requestedUser string, page *paging.Page) (any, gtserror.WithCode) {
// Authenticate incoming request, getting related accounts.
auth, errWithCode := p.authenticate(ctx, requestedUser)
if errWithCode != nil {
return nil, errWithCode
}
- receivingAcct := auth.receivingAcct
+ receiver := auth.receiver
// Parse collection ID from account's following URI.
- collectionID, err := url.Parse(receivingAcct.FollowingURI)
+ collectionID, err := url.Parse(receiver.FollowingURI)
if err != nil {
- err := gtserror.Newf("error parsing account following uri %s: %w", receivingAcct.FollowingURI, err)
+ err := gtserror.Newf("error parsing account following uri %s: %w", receiver.FollowingURI, err)
return nil, gtserror.NewErrorInternalError(err)
}
// Ensure we have stats for this account.
- if err := p.state.DB.PopulateAccountStats(ctx, receivingAcct); err != nil {
- err := gtserror.Newf("error getting stats for account %s: %w", receivingAcct.ID, err)
+ if err := p.state.DB.PopulateAccountStats(ctx, receiver); err != nil {
+ err := gtserror.Newf("error getting stats for account %s: %w", receiver.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
@@ -334,8 +334,8 @@ func (p *Processor) FollowingGet(ctx context.Context, requestedUser string, page
params.ID = collectionID
switch {
- case receivingAcct.IsInstance() ||
- *receivingAcct.Settings.HideCollections:
+ case receiver.IsInstance() ||
+ *receiver.Settings.HideCollections:
// If account that hides collections, or instance
// account (ie., can't post / have relationships),
// just return barest stub of collection.
@@ -345,7 +345,7 @@ func (p *Processor) FollowingGet(ctx context.Context, requestedUser string, page
// If paging disabled, or we're currently handshaking
// the requester, just return collection that links
// to first page (i.e. path below), with no items.
- params.Total = util.Ptr(*receivingAcct.Stats.FollowingCount)
+ params.Total = util.Ptr(*receiver.Stats.FollowingCount)
params.First = new(paging.Page)
params.Query = make(url.Values, 1)
params.Query.Set("limit", "40") // enables paging
@@ -354,7 +354,7 @@ func (p *Processor) FollowingGet(ctx context.Context, requestedUser string, page
default:
// Paging enabled.
// Get page of full follower objects with attached accounts.
- follows, err := p.state.DB.GetAccountFollows(ctx, receivingAcct.ID, page)
+ follows, err := p.state.DB.GetAccountFollows(ctx, receiver.ID, page)
if err != nil {
err := gtserror.Newf("error getting follows: %w", err)
return nil, gtserror.NewErrorInternalError(err)
@@ -371,7 +371,7 @@ func (p *Processor) FollowingGet(ctx context.Context, requestedUser string, page
}
// Start AS collection page params.
- params.Total = util.Ptr(*receivingAcct.Stats.FollowingCount)
+ params.Total = util.Ptr(*receiver.Stats.FollowingCount)
var pageParams ap.CollectionPageParams
pageParams.CollectionParams = params
@@ -416,28 +416,29 @@ func (p *Processor) FollowingGet(ctx context.Context, requestedUser string, page
// FeaturedCollectionGet returns an ordered collection of the requested username's Pinned posts.
// The returned collection have an `items` property which contains an ordered list of status URIs.
-func (p *Processor) FeaturedCollectionGet(ctx context.Context, requestedUser string) (interface{}, gtserror.WithCode) {
+func (p *Processor) FeaturedCollectionGet(ctx context.Context, requestedUser string) (any, gtserror.WithCode) {
// Authenticate incoming request, getting related accounts.
auth, errWithCode := p.authenticate(ctx, requestedUser)
if errWithCode != nil {
return nil, errWithCode
}
- receivingAcct := auth.receivingAcct
+ receiver := auth.receiver
- statuses, err := p.state.DB.GetAccountPinnedStatuses(ctx, receivingAcct.ID)
- if err != nil {
- if !errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorInternalError(err)
- }
+ statuses, err := p.state.DB.GetAccountPinnedStatuses(ctx, receiver.ID)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting pinned statuses: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
}
- collection, err := p.converter.StatusesToASFeaturedCollection(ctx, receivingAcct.FeaturedCollectionURI, statuses)
+ collection, err := p.converter.StatusesToASFeaturedCollection(ctx, receiver.FeaturedCollectionURI, statuses)
if err != nil {
+ err := gtserror.Newf("error converting pinned statuses: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
data, err := ap.Serialize(collection)
if err != nil {
+ err := gtserror.Newf("error serializing: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
diff --git a/internal/processing/fedi/common.go b/internal/processing/fedi/common.go
index fc783f93e..ff6ed6fd4 100644
--- a/internal/processing/fedi/common.go
+++ b/internal/processing/fedi/common.go
@@ -20,7 +20,6 @@ package fedi
import (
"context"
"errors"
- "fmt"
"net/url"
"code.superseriousbusiness.org/gotosocial/internal/db"
@@ -30,21 +29,23 @@ import (
type commonAuth struct {
handshakingURI *url.URL // Set to requestingAcct's URI if we're currently handshaking them.
- requestingAcct *gtsmodel.Account // Remote account making request to this instance.
- receivingAcct *gtsmodel.Account // Local account receiving the request.
+ requester *gtsmodel.Account // Remote account making request to this instance.
+ receiver *gtsmodel.Account // Local account receiving the request.
}
+// authenticate is a util function for authenticating a signed GET
+// request to one of the AP/fedi resources handled in this package.
func (p *Processor) authenticate(ctx context.Context, requestedUser string) (*commonAuth, gtserror.WithCode) {
- // First get the requested (receiving) LOCAL account with username from database.
+ // Get the requested local account
+ // with given username from database.
receiver, err := p.state.DB.GetAccountByUsernameDomain(ctx, requestedUser, "")
- if err != nil {
- if !errors.Is(err, db.ErrNoEntries) {
- // Real db error.
- err = gtserror.Newf("db error getting account %s: %w", requestedUser, err)
- return nil, gtserror.NewErrorInternalError(err)
- }
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err = gtserror.Newf("db error getting account %s: %w", requestedUser, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
- // Account just not found in the db.
+ if receiver == nil {
+ err := gtserror.Newf("account %s not found in the db", requestedUser)
return nil, gtserror.NewErrorNotFound(err)
}
@@ -60,74 +61,44 @@ func (p *Processor) authenticate(ctx context.Context, requestedUser string) (*co
// don't know the requester yet.
return &commonAuth{
handshakingURI: pubKeyAuth.OwnerURI,
- receivingAcct: receiver,
+ receiver: receiver,
}, nil
}
// Get requester from auth.
requester := pubKeyAuth.Owner
- // Ensure block does not exist between receiver and requester.
- blocked, err := p.state.DB.IsEitherBlocked(ctx, receiver.ID, requester.ID)
- if err != nil {
- err := gtserror.Newf("error checking block: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
- } else if blocked {
- const text = "block exists between accounts"
- return nil, gtserror.NewErrorForbidden(errors.New(text))
+ // Check if requester is suspended.
+ switch {
+ case !requester.IsSuspended():
+ // No problem.
+
+ case requester.DeletedSelf():
+ // Requester deleted their own account.
+ // Why are they now requesting something?
+ err := gtserror.Newf("requester %s self-deleted", requester.UsernameDomain())
+ return nil, gtserror.NewErrorUnauthorized(err)
+
+ default:
+ // Admin from our instance likely suspended account.
+ err := gtserror.Newf("requester %s is suspended", requester.UsernameDomain())
+ return nil, gtserror.NewErrorForbidden(err)
}
- return &commonAuth{
- requestingAcct: requester,
- receivingAcct: receiver,
- }, nil
-}
-
-// validateIntReqRequest is a shortcut function
-// for returning an accepted interaction request
-// targeting `requestedUser`.
-func (p *Processor) validateIntReqRequest(
- ctx context.Context,
- requestedUser string,
- intReqID string,
-) (*gtsmodel.InteractionRequest, gtserror.WithCode) {
- // Authenticate incoming request, getting related accounts.
- auth, errWithCode := p.authenticate(ctx, requestedUser)
- if errWithCode != nil {
- return nil, errWithCode
- }
-
- if auth.handshakingURI != nil {
- // We're currently handshaking, which means we don't know
- // this account yet. This should be a very rare race condition.
- err := gtserror.Newf("network race handshaking %s", auth.handshakingURI)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- // Fetch interaction request with the given ID.
- req, err := p.state.DB.GetInteractionRequestByID(ctx, intReqID)
- if err != nil && !errors.Is(err, db.ErrNoEntries) {
- err := gtserror.Newf("db error getting interaction request %s: %w", intReqID, err)
+ // Ensure receiver does not block requester.
+ blocked, err := p.state.DB.IsBlocked(ctx, receiver.ID, requester.ID)
+ if err != nil {
+ err := gtserror.Newf("db error checking block: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- // Ensure that this is an existing
- // and *accepted* interaction request.
- if req == nil || !req.IsAccepted() {
- const text = "interaction request not found"
- return nil, gtserror.NewErrorNotFound(errors.New(text))
- }
-
- // Ensure interaction request was accepted
- // by the account in the request path.
- if req.TargetAccountID != auth.receivingAcct.ID {
- text := fmt.Sprintf(
- "account %s is not targeted by interaction request %s and therefore can't accept it",
- requestedUser, intReqID,
- )
- return nil, gtserror.NewErrorNotFound(errors.New(text))
+ if blocked {
+ var text = requestedUser + " blocks " + requester.Username
+ return nil, gtserror.NewErrorForbidden(errors.New(text))
}
- // All fine.
- return req, nil
+ return &commonAuth{
+ requester: requester,
+ receiver: receiver,
+ }, nil
}
diff --git a/internal/processing/fedi/emoji.go b/internal/processing/fedi/emoji.go
index 8db8b48ea..e7e3ec406 100644
--- a/internal/processing/fedi/emoji.go
+++ b/internal/processing/fedi/emoji.go
@@ -19,38 +19,69 @@ package fedi
import (
"context"
- "fmt"
+ "errors"
"code.superseriousbusiness.org/gotosocial/internal/ap"
+ "code.superseriousbusiness.org/gotosocial/internal/config"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
)
-// EmojiGet handles the GET for a federated emoji originating from this instance.
-func (p *Processor) EmojiGet(ctx context.Context, requestedEmojiID string) (interface{}, gtserror.WithCode) {
- if _, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, ""); errWithCode != nil {
+// EmojiGet handles the GET for an emoji originating from this instance.
+func (p *Processor) EmojiGet(ctx context.Context, emojiID string) (any, gtserror.WithCode) {
+ // Authenticate incoming request.
+ //
+ // Pass hostname string to this function to indicate
+ // it's the instance account being requested, as
+ // emojis are always owned by the instance account.
+ auth, errWithCode := p.authenticate(ctx, config.GetHost())
+ if errWithCode != nil {
return nil, errWithCode
}
- requestedEmoji, err := p.state.DB.GetEmojiByID(ctx, requestedEmojiID)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting emoji with id %s: %s", requestedEmojiID, err))
+ if auth.handshakingURI != nil {
+ // We're currently handshaking, which means
+ // we don't know this account yet. This should
+ // be a very rare race condition.
+ err := gtserror.Newf("network race handshaking %s", auth.handshakingURI)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Get the requested emoji.
+ emoji, err := p.state.DB.GetEmojiByID(ctx, emojiID)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting emoji %s: %w", emojiID, err)
+ return nil, gtserror.NewErrorNotFound(err)
}
- if !requestedEmoji.IsLocal() {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji with id %s doesn't belong to this instance (domain %s)", requestedEmojiID, requestedEmoji.Domain))
+ if emoji == nil {
+ err := gtserror.Newf("emoji %s not found in the db", emojiID)
+ return nil, gtserror.NewErrorNotFound(err)
}
- if *requestedEmoji.Disabled {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji with id %s has been disabled", requestedEmojiID))
+ // Only serve *our*
+ // emojis on this path.
+ if !emoji.IsLocal() {
+ err := gtserror.Newf("emoji %s doesn't belong to this instance (domain is %s)", emojiID, emoji.Domain)
+ return nil, gtserror.NewErrorNotFound(err)
}
- apEmoji, err := p.converter.EmojiToAS(ctx, requestedEmoji)
+ // Don't serve emojis that have
+ // been disabled by an admin.
+ if *emoji.Disabled {
+ err := gtserror.Newf("emoji with id %s has been disabled by an admin", emojiID)
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+
+ apEmoji, err := p.converter.EmojiToAS(ctx, emoji)
if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting gtsmodel emoji with id %s to ap emoji: %s", requestedEmojiID, err))
+ err := gtserror.Newf("error converting emoji %s to ap: %s", emojiID, err)
+ return nil, gtserror.NewErrorInternalError(err)
}
data, err := ap.Serialize(apEmoji)
if err != nil {
+ err := gtserror.Newf("error serializing emoji %s: %w", emojiID, err)
return nil, gtserror.NewErrorInternalError(err)
}
diff --git a/internal/processing/fedi/status.go b/internal/processing/fedi/status.go
index 497bcc177..d1de6f4c1 100644
--- a/internal/processing/fedi/status.go
+++ b/internal/processing/fedi/status.go
@@ -26,6 +26,7 @@ import (
"code.superseriousbusiness.org/activity/streams/vocab"
"code.superseriousbusiness.org/gotosocial/internal/ap"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/log"
@@ -33,9 +34,13 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/util"
)
-// StatusGet handles the getting of a fedi/activitypub representation of a local status.
+// StatusGet handles getting an AP representation of a local status.
// It performs appropriate authentication before returning a JSON serializable interface.
-func (p *Processor) StatusGet(ctx context.Context, requestedUser string, statusID string) (interface{}, gtserror.WithCode) {
+func (p *Processor) StatusGet(
+ ctx context.Context,
+ requestedUser string,
+ statusID string,
+) (any, gtserror.WithCode) {
// Authenticate incoming request, getting related accounts.
auth, errWithCode := p.authenticate(ctx, requestedUser)
if errWithCode != nil {
@@ -49,16 +54,23 @@ func (p *Processor) StatusGet(ctx context.Context, requestedUser string, statusI
err := gtserror.Newf("network race handshaking %s", auth.handshakingURI)
return nil, gtserror.NewErrorInternalError(err)
}
-
- receivingAcct := auth.receivingAcct
- requestingAcct := auth.requestingAcct
+ receiver := auth.receiver
+ requester := auth.requester
status, err := p.state.DB.GetStatusByID(ctx, statusID)
- if err != nil {
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting status: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if status == nil {
+ // TODO: Update this to serve "gone"
+ // when a status has been deleted.
+ err := gtserror.Newf("status %s not found in the db", statusID)
return nil, gtserror.NewErrorNotFound(err)
}
- if status.AccountID != receivingAcct.ID {
+ if status.AccountID != receiver.ID {
const text = "status does not belong to receiving account"
return nil, gtserror.NewErrorNotFound(errors.New(text))
}
@@ -68,7 +80,7 @@ func (p *Processor) StatusGet(ctx context.Context, requestedUser string, statusI
return nil, gtserror.NewErrorNotFound(errors.New(text))
}
- visible, err := p.visFilter.StatusVisible(ctx, requestingAcct, status)
+ visible, err := p.visFilter.StatusVisible(ctx, requester, status)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
@@ -93,7 +105,7 @@ func (p *Processor) StatusGet(ctx context.Context, requestedUser string, statusI
return data, nil
}
-// GetStatus handles the getting of a fedi/activitypub representation of replies to a status,
+// GetStatus handles getting an AP representation of replies to a status,
// performing appropriate authentication before returning a JSON serializable interface to the caller.
func (p *Processor) StatusRepliesGet(
ctx context.Context,
@@ -101,7 +113,7 @@ func (p *Processor) StatusRepliesGet(
statusID string,
page *paging.Page,
onlyOtherAccounts bool,
-) (interface{}, gtserror.WithCode) {
+) (any, gtserror.WithCode) {
// Authenticate incoming request, getting related accounts.
auth, errWithCode := p.authenticate(ctx, requestedUser)
if errWithCode != nil {
@@ -116,8 +128,8 @@ func (p *Processor) StatusRepliesGet(
return nil, gtserror.NewErrorInternalError(err)
}
- receivingAcct := auth.receivingAcct
- requestingAcct := auth.requestingAcct
+ receivingAcct := auth.receiver
+ requestingAcct := auth.requester
// Get target status and ensure visible to requester.
status, errWithCode := p.c.GetVisibleTargetStatus(ctx,
diff --git a/internal/processing/fedi/user.go b/internal/processing/fedi/user.go
index 53dfd6022..9fb338673 100644
--- a/internal/processing/fedi/user.go
+++ b/internal/processing/fedi/user.go
@@ -20,96 +20,83 @@ package fedi
import (
"context"
"errors"
- "fmt"
- "net/url"
"code.superseriousbusiness.org/gotosocial/internal/ap"
"code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
- "code.superseriousbusiness.org/gotosocial/internal/uris"
)
-// UserGet handles the getting of a fedi/activitypub representation of a user/account,
-// performing authentication before returning a JSON serializable interface to the caller.
-func (p *Processor) UserGet(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
- // (Try to) get the requested local account from the db.
- receiver, err := p.state.DB.GetAccountByUsernameDomain(ctx, requestedUsername, "")
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- // Account just not found w/ this username.
- err := fmt.Errorf("account with username %s not found in the db", requestedUsername)
- return nil, gtserror.NewErrorNotFound(err)
- }
+// UserGet handles getting an AP representation of an account.
+// It does auth before returning a JSON serializable interface to the caller.
+func (p *Processor) UserGet(
+ ctx context.Context,
+ requestedUser string,
+) (any, gtserror.WithCode) {
+ // Authenticate incoming request, getting related accounts.
+ //
+ // We may currently be handshaking with the remote account
+ // making the request. Unlike with other fedi endpoints,
+ // don't bother checking this; if we're still handshaking
+ // just serve the AP representation of our account anyway.
+ //
+ // This ensures that we don't get stuck in a loop with another
+ // GtS instance, where each instance is trying repeatedly to
+ // dereference the other account that's making the request
+ // before it will reveal its own account.
+ //
+ // Instead, we end up in an 'I'll show you mine if you show me
+ // yours' situation, where we sort of agree to reveal each
+ // other's profiles at the same time.
+ auth, errWithCode := p.authenticate(ctx, requestedUser)
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
- // Real db error.
- err := fmt.Errorf("db error getting account with username %s: %w", requestedUsername, err)
+ // Generate the proper AP representation.
+ accountable, err := p.converter.AccountToAS(ctx, auth.receiver)
+ if err != nil {
+ err := gtserror.Newf("error converting to accountable: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- if uris.IsPublicKeyPath(requestURL) {
- // If request is on a public key path, we don't need to
- // authenticate this request. However, we'll only serve
- // the bare minimum user profile needed for the pubkey.
- //
- // TODO: https://codeberg.org/superseriousbusiness/gotosocial/issues/1186
- minimalPerson, err := p.converter.AccountToASMinimal(ctx, receiver)
- if err != nil {
- err := gtserror.Newf("error converting to minimal account: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- // Return early with bare minimum data.
- return data(minimalPerson)
+ data, err := ap.Serialize(accountable)
+ if err != nil {
+ err := gtserror.Newf("error serializing accountable: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
}
- // If the request is not on a public key path, we want to
- // try to authenticate it before we serve any data, so that
- // we can serve a more complete profile.
- pubKeyAuth, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
- if errWithCode != nil {
- return nil, errWithCode // likely 401
- }
+ return data, nil
+}
- // Auth passed, generate the proper AP representation.
- accountable, err := p.converter.AccountToAS(ctx, receiver)
- if err != nil {
- err := gtserror.Newf("error converting account: %w", err)
+// UserGetMinimal returns a minimal AP representation
+// of the requested account, containing just the public
+// key, without doing authentication.
+func (p *Processor) UserGetMinimal(
+ ctx context.Context,
+ requestedUser string,
+) (any, gtserror.WithCode) {
+ acct, err := p.state.DB.GetAccountByUsernameDomain(
+ gtscontext.SetBarebones(ctx),
+ requestedUser, "",
+ )
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting account %s: %w", requestedUser, err)
return nil, gtserror.NewErrorInternalError(err)
}
- if pubKeyAuth.Handshaking {
- // If we are currently handshaking with the remote account
- // making the request, then don't be coy: just serve the AP
- // representation of the target account.
- //
- // This handshake check ensures that we don't get stuck in
- // a loop with another GtS instance, where each instance is
- // trying repeatedly to dereference the other account that's
- // making the request before it will reveal its own account.
- //
- // Instead, we end up in an 'I'll show you mine if you show me
- // yours' situation, where we sort of agree to reveal each
- // other's profiles at the same time.
- return data(accountable)
+ if acct == nil {
+ err := gtserror.Newf("account %s not found in the db", requestedUser)
+ return nil, gtserror.NewErrorNotFound(err)
}
- // Get requester from auth.
- requester := pubKeyAuth.Owner
-
- // Check that block does not exist between receiver and requester.
- blocked, err := p.state.DB.IsBlocked(ctx, receiver.ID, requester.ID)
+ // Generate minimal AP representation.
+ accountable, err := p.converter.AccountToASMinimal(ctx, acct)
if err != nil {
- err := gtserror.Newf("error checking block: %w", err)
+ err := gtserror.Newf("error converting to accountable: %w", err)
return nil, gtserror.NewErrorInternalError(err)
- } else if blocked {
- const text = "block exists between accounts"
- return nil, gtserror.NewErrorForbidden(errors.New(text))
}
- return data(accountable)
-}
-
-func data(accountable ap.Accountable) (interface{}, gtserror.WithCode) {
data, err := ap.Serialize(accountable)
if err != nil {
err := gtserror.Newf("error serializing accountable: %w", err)
diff --git a/internal/processing/fedi/wellknown.go b/internal/processing/fedi/wellknown.go
index 0e6989803..236f09257 100644
--- a/internal/processing/fedi/wellknown.go
+++ b/internal/processing/fedi/wellknown.go
@@ -47,7 +47,7 @@ var (
nodeInfoProtocols = []string{"activitypub"}
nodeInfoInbound = []string{}
nodeInfoOutbound = []string{}
- nodeInfoMetadata = make(map[string]interface{})
+ nodeInfoMetadata = make(map[string]any)
)
// NodeInfoRelGet returns a well known response giving the path to node info.
@@ -156,11 +156,12 @@ func (p *Processor) HostMetaGet() *apimodel.HostMeta {
}
// WebfingerGet handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups.
-func (p *Processor) WebfingerGet(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode) {
+func (p *Processor) WebfingerGet(ctx context.Context, requestedUser string) (*apimodel.WellKnownResponse, gtserror.WithCode) {
// Get the local account the request is referring to.
- requestedAccount, err := p.state.DB.GetAccountByUsernameDomain(ctx, requestedUsername, "")
+ requestedAccount, err := p.state.DB.GetAccountByUsernameDomain(ctx, requestedUser, "")
if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
+ err := gtserror.Newf("db error getting account %s: %s", requestedUser, err)
+ return nil, gtserror.NewErrorNotFound(err)
}
return &apimodel.WellKnownResponse{
diff --git a/internal/web/customcss.go b/internal/web/customcss.go
index f0c767503..f33164f1e 100644
--- a/internal/web/customcss.go
+++ b/internal/web/customcss.go
@@ -34,7 +34,7 @@ func (m *Module) customCSSGETHandler(c *gin.Context) {
return
}
- targetUsername, errWithCode := apiutil.ParseUsername(c.Param(apiutil.UsernameKey))
+ requestedUser, errWithCode := apiutil.ParseUsername(c.Param(apiutil.UsernameKey))
if errWithCode != nil {
apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
@@ -45,7 +45,7 @@ func (m *Module) customCSSGETHandler(c *gin.Context) {
// when custom CSS gets toggled on or off.
var customCSS string
if config.GetAccountsAllowCustomCSS() {
- customCSS, errWithCode = m.processor.Account().GetCustomCSSForUsername(c.Request.Context(), targetUsername)
+ customCSS, errWithCode = m.processor.Account().GetCustomCSSForUsername(c.Request.Context(), requestedUser)
if errWithCode != nil {
apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
diff --git a/internal/web/profile.go b/internal/web/profile.go
index 458557b8b..e619961d7 100644
--- a/internal/web/profile.go
+++ b/internal/web/profile.go
@@ -19,7 +19,6 @@ package web
import (
"context"
- "fmt"
"net/http"
"strings"
@@ -63,27 +62,32 @@ func (m *Module) prepareProfile(c *gin.Context) *profile {
}
// Parse + normalize account username from the URL.
- requestedUsername, errWithCode := apiutil.ParseUsername(c.Param(apiutil.UsernameKey))
+ requestedUser, errWithCode := apiutil.ParseUsername(c.Param(apiutil.UsernameKey))
if errWithCode != nil {
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
return nil
}
- requestedUsername = strings.ToLower(requestedUsername)
+ requestedUser = strings.ToLower(requestedUser)
// Check what type of content is being requested.
// If we're getting an AP request on this endpoint
// we should render the AP representation instead.
- contentType, err := apiutil.NegotiateAccept(c, apiutil.HTMLOrActivityPubHeaders...)
+ accept, err := apiutil.NegotiateAccept(c, apiutil.HTMLOrActivityPubHeaders...)
if err != nil {
apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), instanceGet)
return nil
}
- if contentType == string(apiutil.AppActivityJSON) ||
- contentType == string(apiutil.AppActivityLDJSON) {
+ if apiutil.ASContentType(accept) {
// AP account representation has
// been requested, return that.
- m.returnAPAccount(c, requestedUsername, contentType)
+ user, errWithCode := m.processor.Fedi().UserGet(c.Request.Context(), requestedUser)
+ if errWithCode != nil {
+ apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return nil
+ }
+
+ apiutil.JSONType(c, http.StatusOK, accept, user)
return nil
}
@@ -91,7 +95,7 @@ func (m *Module) prepareProfile(c *gin.Context) *profile {
//
// Proceed with getting the web
// representation of the account.
- account, errWithCode := m.processor.Account().GetWeb(ctx, requestedUsername)
+ account, errWithCode := m.processor.Account().GetWeb(ctx, requestedUser)
if errWithCode != nil {
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
return nil
@@ -102,7 +106,7 @@ func (m *Module) prepareProfile(c *gin.Context) *profile {
//
// TODO: change this to 410?
if account.Suspended {
- err := fmt.Errorf("target account %s is suspended", requestedUsername)
+ err := gtserror.Newf("target account %s is suspended", requestedUser)
apiutil.WebErrorHandler(c, gtserror.NewErrorNotFound(err), instanceGet)
return nil
}
@@ -342,19 +346,3 @@ func (m *Module) profileGallery(c *gin.Context, p *profile) {
apiutil.TemplateWebPage(c, page)
}
-
-// returnAPAccount returns an ActivityPub representation of
-// target account. It will do http signature authentication.
-func (m *Module) returnAPAccount(
- c *gin.Context,
- targetUsername string,
- contentType string,
-) {
- user, errWithCode := m.processor.Fedi().UserGet(c.Request.Context(), targetUsername, c.Request.URL)
- if errWithCode != nil {
- apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
- return
- }
-
- apiutil.JSONType(c, http.StatusOK, contentType, user)
-}
diff --git a/internal/web/thread.go b/internal/web/thread.go
index 3844bc6f3..fa3e54fac 100644
--- a/internal/web/thread.go
+++ b/internal/web/thread.go
@@ -19,8 +19,6 @@ package web
import (
"context"
- "encoding/json"
- "fmt"
"net/http"
"strings"
@@ -47,14 +45,14 @@ func (m *Module) threadGETHandler(c *gin.Context) {
return instance, nil
}
- // Parse account targetUsername and status ID from the URL.
- targetUsername, errWithCode := apiutil.ParseUsername(c.Param(apiutil.UsernameKey))
+ // Parse account requestedUser and status ID from the URL.
+ requestedUser, errWithCode := apiutil.ParseUsername(c.Param(apiutil.UsernameKey))
if errWithCode != nil {
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
return
}
- targetStatusID, errWithCode := apiutil.ParseWebStatusID(c.Param(apiutil.WebStatusIDKey))
+ statusID, errWithCode := apiutil.ParseWebStatusID(c.Param(apiutil.WebStatusIDKey))
if errWithCode != nil {
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
return
@@ -67,8 +65,8 @@ func (m *Module) threadGETHandler(c *gin.Context) {
//
// todo: Update this logic when different username patterns
// are allowed, and/or when status slugs are introduced.
- targetUsername = strings.ToLower(targetUsername)
- targetStatusID = strings.ToUpper(targetStatusID)
+ requestedUser = strings.ToLower(requestedUser)
+ statusID = strings.ToUpper(statusID)
// Check what type of content is being requested. If we're getting an AP
// request on this endpoint we should render the AP representation instead.
@@ -78,38 +76,44 @@ func (m *Module) threadGETHandler(c *gin.Context) {
return
}
- if accept == string(apiutil.AppActivityJSON) || accept == string(apiutil.AppActivityLDJSON) {
+ if apiutil.ASContentType(accept) {
// AP status representation has been requested.
- m.returnAPStatus(c, targetUsername, targetStatusID, accept, instanceGet)
+ status, errWithCode := m.processor.Fedi().StatusGet(c.Request.Context(), requestedUser, statusID)
+ if errWithCode != nil {
+ apiutil.WebErrorHandler(c, errWithCode, instanceGet)
+ return
+ }
+
+ apiutil.JSONType(c, http.StatusOK, accept, status)
return
}
// text/html has been requested. Proceed with getting the web view of the status.
// Fetch the target account so we can do some checks on it.
- targetAccount, errWithCode := m.processor.Account().GetWeb(ctx, targetUsername)
+ acct, errWithCode := m.processor.Account().GetWeb(ctx, requestedUser)
if errWithCode != nil {
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
return
}
- // If target account is suspended, this page should not be visible.
- if targetAccount.Suspended {
- err := fmt.Errorf("target account %s is suspended", targetUsername)
+ // If requested account is suspended, this page should not be visible.
+ if acct.Suspended {
+ err := gtserror.Newf("account %s is suspended", requestedUser)
apiutil.WebErrorHandler(c, gtserror.NewErrorNotFound(err), instanceGet)
return
}
- // Get the thread context. This will fetch the target status as well.
- context, errWithCode := m.processor.Status().WebContextGet(ctx, targetStatusID)
+ // Get the thread context. This will fetch the status as well.
+ context, errWithCode := m.processor.Status().WebContextGet(ctx, statusID)
if errWithCode != nil {
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
return
}
- // Ensure status actually belongs to target account.
- if context.Status.Account.ID != targetAccount.ID {
- err := fmt.Errorf("target account %s does not own status %s", targetUsername, targetStatusID)
+ // Ensure status actually belongs to requested account.
+ if context.Status.Account.ID != acct.ID {
+ err := gtserror.Newf("account %s does not own status %s", requestedUser, statusID)
apiutil.WebErrorHandler(c, gtserror.NewErrorNotFound(err), instanceGet)
return
}
@@ -128,7 +132,7 @@ func (m *Module) threadGETHandler(c *gin.Context) {
)
// User-selected theme if set.
- if theme := targetAccount.Theme; theme != "" {
+ if theme := acct.Theme; theme != "" {
stylesheets = append(
stylesheets,
themesPathPrefix+"/"+theme,
@@ -138,7 +142,7 @@ func (m *Module) threadGETHandler(c *gin.Context) {
// Custom CSS for this user last in cascade.
stylesheets = append(
stylesheets,
- "/@"+targetAccount.Username+"/custom.css",
+ "/@"+acct.Username+"/custom.css",
)
page := apiutil.WebPage{
@@ -164,28 +168,3 @@ func (m *Module) threadGETHandler(c *gin.Context) {
apiutil.TemplateWebPage(c, page)
}
-
-// returnAPStatus returns an ActivityPub representation of target status,
-// created by targetUsername. It will do http signature authentication.
-func (m *Module) returnAPStatus(
- c *gin.Context,
- targetUsername string,
- targetStatusID string,
- accept string,
- instanceGet func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode),
-) {
- status, errWithCode := m.processor.Fedi().StatusGet(c.Request.Context(), targetUsername, targetStatusID)
- if errWithCode != nil {
- apiutil.WebErrorHandler(c, errWithCode, instanceGet)
- return
- }
-
- b, err := json.Marshal(status)
- if err != nil {
- err := gtserror.Newf("could not marshal json: %w", err)
- apiutil.WebErrorHandler(c, gtserror.NewErrorInternalError(err), instanceGet)
- return
- }
-
- c.Data(http.StatusOK, accept, b)
-}