diff options
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 := >smodel.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 := >smodel.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) -} |
