From 47051a26d6ac1ea6c69b5bc6a58a16ed39486486 Mon Sep 17 00:00:00 2001 From: tobi Date: Wed, 8 Oct 2025 13:14:06 +0200 Subject: [PATCH] start refactoring return codes from fedi endpoints, remove some cruft --- internal/api/activitypub/emoji/emoji.go | 8 +- internal/api/activitypub/emoji/emojiget.go | 6 +- .../api/activitypub/emoji/emojiget_test.go | 3 +- .../api/activitypub/publickey/publickey.go | 9 +- .../api/activitypub/publickey/publickeyget.go | 10 +- internal/api/activitypub/users/common.go | 4 +- internal/api/activitypub/users/featured.go | 2 +- internal/api/activitypub/users/followers.go | 2 +- internal/api/activitypub/users/following.go | 2 +- .../api/activitypub/users/inboxpost_test.go | 4 +- internal/api/activitypub/users/outboxget.go | 2 +- .../api/activitypub/users/outboxget_test.go | 18 +-- internal/api/activitypub/users/repliesget.go | 4 +- .../api/activitypub/users/repliesget_test.go | 20 +-- internal/api/activitypub/users/statusget.go | 4 +- .../api/activitypub/users/statusget_test.go | 14 +- internal/api/activitypub/users/user.go | 42 ++---- internal/api/activitypub/users/userget.go | 10 +- .../api/activitypub/users/userget_test.go | 9 +- internal/federation/authenticate.go | 57 +++++-- internal/federation/federatingprotocol.go | 22 +-- internal/processing/fedi/collections.go | 85 +++++------ internal/processing/fedi/common.go | 50 ++++--- internal/processing/fedi/emoji.go | 69 ++++++--- internal/processing/fedi/status.go | 36 +++-- internal/processing/fedi/user.go | 141 ++++++++---------- internal/processing/fedi/wellknown.go | 2 +- internal/web/profile.go | 2 +- 28 files changed, 346 insertions(+), 291 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..6a554f10f 100644 --- a/internal/api/activitypub/publickey/publickeyget.go +++ b/internal/api/activitypub/publickey/publickeyget.go @@ -34,7 +34,7 @@ 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)) + requestedUsername := strings.ToLower(c.Param(apiutil.UsernameKey)) if requestedUsername == "" { err := errors.New("no username specified in request") apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) @@ -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) return } - resp, errWithCode := m.processor.Fedi().UserGet(c.Request.Context(), requestedUsername, c.Request.URL) + resp, errWithCode := m.processor.Fedi().UserGetMinimal( + c.Request.Context(), + requestedUsername, + ) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/activitypub/users/common.go b/internal/api/activitypub/users/common.go index a10b99d95..87121a440 100644 --- a/internal/api/activitypub/users/common.go +++ b/internal/api/activitypub/users/common.go @@ -24,7 +24,7 @@ type SwaggerCollection struct { // A string or an array of strings, or more // complex nested items. // example: https://www.w3.org/ns/activitystreams - Context interface{} `json:"@context"` + Context any `json:"@context"` // ActivityStreams ID. // example: https://example.org/users/some_user/statuses/106717595988259568/replies ID string `json:"id"` @@ -64,7 +64,7 @@ type SwaggerFeaturedCollection struct { // A string or an array of strings, or more // complex nested items. // example: https://www.w3.org/ns/activitystreams - Context interface{} `json:"@context"` + Context any `json:"@context"` // ActivityStreams ID. // example: https://example.org/users/some_user/collections/featured ID string `json:"id"` diff --git a/internal/api/activitypub/users/featured.go b/internal/api/activitypub/users/featured.go index 90b368b26..e9d125409 100644 --- a/internal/api/activitypub/users/featured.go +++ b/internal/api/activitypub/users/featured.go @@ -67,7 +67,7 @@ import ( // description: not found func (m *Module) FeaturedCollectionGETHandler(c *gin.Context) { // usernames on our instance are always lowercase - requestedUsername := strings.ToLower(c.Param(UsernameKey)) + requestedUsername := strings.ToLower(c.Param(apiutil.UsernameKey)) if requestedUsername == "" { err := errors.New("no username specified in request") apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) diff --git a/internal/api/activitypub/users/followers.go b/internal/api/activitypub/users/followers.go index f00b7bc40..2b0aacf1a 100644 --- a/internal/api/activitypub/users/followers.go +++ b/internal/api/activitypub/users/followers.go @@ -31,7 +31,7 @@ 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)) + requestedUsername := strings.ToLower(c.Param(apiutil.UsernameKey)) if requestedUsername == "" { err := errors.New("no username specified in request") apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) diff --git a/internal/api/activitypub/users/following.go b/internal/api/activitypub/users/following.go index 630e0b821..feb60806d 100644 --- a/internal/api/activitypub/users/following.go +++ b/internal/api/activitypub/users/following.go @@ -31,7 +31,7 @@ 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)) + requestedUsername := strings.ToLower(c.Param(apiutil.UsernameKey)) if requestedUsername == "" { err := errors.New("no username specified in request") apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) 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..5ddd6dd26 100644 --- a/internal/api/activitypub/users/outboxget.go +++ b/internal/api/activitypub/users/outboxget.go @@ -84,7 +84,7 @@ import ( // description: not found func (m *Module) OutboxGETHandler(c *gin.Context) { // usernames on our instance are always lowercase - requestedUsername := strings.ToLower(c.Param(UsernameKey)) + requestedUsername := strings.ToLower(c.Param(apiutil.UsernameKey)) if requestedUsername == "" { err := errors.New("no username specified in request") apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) 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..5ef03f6ee 100644 --- a/internal/api/activitypub/users/repliesget.go +++ b/internal/api/activitypub/users/repliesget.go @@ -91,7 +91,7 @@ import ( // description: not found func (m *Module) StatusRepliesGETHandler(c *gin.Context) { // usernames on our instance are always lowercase - requestedUsername := strings.ToLower(c.Param(UsernameKey)) + requestedUsername := strings.ToLower(c.Param(apiutil.UsernameKey)) if requestedUsername == "" { err := errors.New("no username specified in request") apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) @@ -99,7 +99,7 @@ func (m *Module) StatusRepliesGETHandler(c *gin.Context) { } // 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) 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..26385fcdf 100644 --- a/internal/api/activitypub/users/statusget.go +++ b/internal/api/activitypub/users/statusget.go @@ -30,7 +30,7 @@ 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)) + requestedUsername := strings.ToLower(c.Param(apiutil.UsernameKey)) if requestedUsername == "" { err := errors.New("no username specified in request") apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) @@ -38,7 +38,7 @@ func (m *Module) StatusGETHandler(c *gin.Context) { } // 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) 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..f77a1ce71 100644 --- a/internal/api/activitypub/users/userget.go +++ b/internal/api/activitypub/users/userget.go @@ -38,7 +38,7 @@ import ( // request is blocked. func (m *Module) UsersGETHandler(c *gin.Context) { // usernames on our instance are always lowercase - requestedUsername := strings.ToLower(c.Param(UsernameKey)) + requestedUsername := strings.ToLower(c.Param(apiutil.UsernameKey)) if requestedUsername == "" { err := errors.New("no username specified in request") apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) @@ -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) return } - resp, errWithCode := m.processor.Fedi().UserGet(c.Request.Context(), requestedUsername, c.Request.URL) + resp, errWithCode := m.processor.Fedi().UserGet( + c.Request.Context(), + requestedUsername, + ) 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/federation/authenticate.go b/internal/federation/authenticate.go index f8515e649..7faa55b04 100644 --- a/internal/federation/authenticate.go +++ b/internal/federation/authenticate.go @@ -206,17 +206,54 @@ func (f *Federator) AuthenticateFederatedRequest(ctx context.Context, requestedU 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) - } + // Check if a status code was returned + // from the failed dereference attempt. + switch statusCode := gtserror.StatusCode(err); statusCode { - err := gtserror.Newf("error dereferencing account %s: %w", pubKeyAuth.OwnerURI, err) - return nil, gtserror.NewErrorInternalError(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. + err := gtserror.Newf( + "received 403 Forbidden fetching account %s, cannot process request: %w", + pubKeyAuth.OwnerURI, err, + ) + return nil, gtserror.NewErrorUnauthorized(err) + + 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. + err := gtserror.Newf( + "received 401 Unauthorized fetching account %s, cannot process request: %w", + pubKeyAuth.OwnerURI, err, + ) + return nil, gtserror.NewErrorUnauthorized(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. + err := gtserror.Newf( + "requesting account %s is gone, cannot process request: %w", + pubKeyAuth.OwnerURI, err, + ) + return nil, gtserror.NewErrorGone(err) + + default: + // In all other cases, return 401 Unauthorized, + // as we could not continue with this request. + err := gtserror.Newf( + "could not dereference requesting account %s: %w", + pubKeyAuth.OwnerURI, err, + ) + return nil, gtserror.NewErrorUnauthorized(err) + } } // Catch a possible (but very rare) race condition where diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go index e1ec86b32..cce00856a 100644 --- a/internal/federation/federatingprotocol.go +++ b/internal/federation/federatingprotocol.go @@ -217,17 +217,21 @@ func (f *Federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr pubKeyAuth, errWithCode := f.AuthenticateFederatedRequest(ctx, receivingAccount.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. + + // If codes 400, 401, or 403, obey the go-fed + // interface by writing the header and bailing. + case http.StatusUnauthorized, + http.StatusForbidden, + http.StatusBadRequest: w.WriteHeader(errWithCode.Code()) + + // If the requesting account's key 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. case http.StatusGone: - // If the requesting account's key 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. w.WriteHeader(http.StatusAccepted) } 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..02159b72b 100644 --- a/internal/processing/fedi/common.go +++ b/internal/processing/fedi/common.go @@ -30,24 +30,11 @@ 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. } func (p *Processor) authenticate(ctx context.Context, requestedUser string) (*commonAuth, gtserror.WithCode) { - // First get the requested (receiving) LOCAL account with 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) - } - - // Account just not found in the db. - return nil, gtserror.NewErrorNotFound(err) - } - // Ensure request signed, and use signature URI to // get requesting account, dereferencing if necessary. pubKeyAuth, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUser) @@ -55,31 +42,46 @@ func (p *Processor) authenticate(ctx context.Context, requestedUser string) (*co return nil, errWithCode } + // Get the requested local account + // with given username from database. + receiver, err := p.state.DB.GetAccountByUsernameDomain(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 receiver == nil { + err := gtserror.Newf("account %s not found in the db", requestedUser) + return nil, gtserror.NewErrorNotFound(err) + } + if pubKeyAuth.Handshaking { // We're still handshaking so we // 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) + // Ensure receiver does not block requester. + blocked, err := p.state.DB.IsBlocked(ctx, receiver.ID, requester.ID) if err != nil { - err := gtserror.Newf("error checking block: %w", err) + err := gtserror.Newf("db error checking block: %w", err) return nil, gtserror.NewErrorInternalError(err) - } else if blocked { - const text = "block exists between accounts" + } + + if blocked { + var text = requestedUser + " blocks " + requester.Username return nil, gtserror.NewErrorForbidden(errors.New(text)) } return &commonAuth{ - requestingAcct: requester, - receivingAcct: receiver, + requester: requester, + receiver: receiver, }, nil } @@ -120,7 +122,7 @@ func (p *Processor) validateIntReqRequest( // Ensure interaction request was accepted // by the account in the request path. - if req.TargetAccountID != auth.receivingAcct.ID { + 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, 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 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 emoji == nil { + err := gtserror.Newf("emoji %s not found in the db", emojiID) + return nil, gtserror.NewErrorNotFound(err) + } + + // 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) + } + + // 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.NewErrorNotFound(fmt.Errorf("database error getting emoji with id %s: %s", requestedEmojiID, 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 *requestedEmoji.Disabled { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji with id %s has been disabled", requestedEmojiID)) - } - - apEmoji, err := p.converter.EmojiToAS(ctx, requestedEmoji) - 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..a08c963f0 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) - } - - // Real db error. - err := fmt.Errorf("db error getting account with username %s: %w", requestedUsername, 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) - } - - // 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) +// 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, + // if we're still handshaking then don't be coy: just serve + // the AP representation of the requested 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 // likely 401 + return nil, errWithCode } - // Auth passed, generate the proper AP representation. - accountable, err := p.converter.AccountToAS(ctx, receiver) + // Generate the proper AP representation. + accountable, err := p.converter.AccountToAS(ctx, auth.receiver) if err != nil { - err := gtserror.Newf("error converting account: %w", err) + err := gtserror.Newf("error converting to accountable: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + + data, err := ap.Serialize(accountable) + if err != nil { + err := gtserror.Newf("error serializing accountable: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + + return data, nil +} + +// 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 acct == nil { + err := gtserror.Newf("account %s not found in the db", requestedUser) + return nil, gtserror.NewErrorNotFound(err) + } + + // Generate minimal AP representation. + accountable, err := p.converter.AccountToASMinimal(ctx, acct) + if err != nil { + err := gtserror.Newf("error converting to accountable: %w", 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) - } - - // 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) - 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)) - } - - 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..ff9be0522 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. diff --git a/internal/web/profile.go b/internal/web/profile.go index 458557b8b..747ef88b8 100644 --- a/internal/web/profile.go +++ b/internal/web/profile.go @@ -350,7 +350,7 @@ func (m *Module) returnAPAccount( targetUsername string, contentType string, ) { - user, errWithCode := m.processor.Fedi().UserGet(c.Request.Context(), targetUsername, c.Request.URL) + user, errWithCode := m.processor.Fedi().UserGet(c.Request.Context(), targetUsername) if errWithCode != nil { apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return