mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2025-06-05 21:59:39 +02:00
[feature] Implement /api/v2/instance
endpoint (#1409)
* interim: start adding /api/v2/instance * finish up
This commit is contained in:
@@ -26,12 +26,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// InstanceInformationPath is for serving instance info requests, minus the 'api' prefix.
|
||||
InstanceInformationPath = "/v1/instance"
|
||||
// InstancePeersPath is for serving instance peers requests.
|
||||
InstancePeersPath = InstanceInformationPath + "/peers"
|
||||
// PeersFilterKey is used to provide filters to /api/v1/instance/peers
|
||||
PeersFilterKey = "filter"
|
||||
InstanceInformationPathV1 = "/v1/instance"
|
||||
InstanceInformationPathV2 = "/v2/instance"
|
||||
InstancePeersPath = InstanceInformationPathV1 + "/peers"
|
||||
PeersFilterKey = "filter" // PeersFilterKey is used to provide filters to /api/v1/instance/peers
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
@@ -45,7 +43,9 @@ func New(processor processing.Processor) *Module {
|
||||
}
|
||||
|
||||
func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) {
|
||||
attachHandler(http.MethodGet, InstanceInformationPath, m.InstanceInformationGETHandler)
|
||||
attachHandler(http.MethodPatch, InstanceInformationPath, m.InstanceUpdatePATCHHandler)
|
||||
attachHandler(http.MethodGet, InstanceInformationPathV1, m.InstanceInformationGETHandlerV1)
|
||||
attachHandler(http.MethodGet, InstanceInformationPathV2, m.InstanceInformationGETHandlerV2)
|
||||
|
||||
attachHandler(http.MethodPatch, InstanceInformationPathV1, m.InstanceUpdatePATCHHandler)
|
||||
attachHandler(http.MethodGet, InstancePeersPath, m.InstancePeersGETHandler)
|
||||
}
|
||||
|
@@ -22,13 +22,12 @@ import (
|
||||
"net/http"
|
||||
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// InstanceInformationGETHandler swagger:operation GET /api/v1/instance instanceGet
|
||||
// InstanceInformationV1GETHandlerV1 swagger:operation GET /api/v1/instance instanceGetV1
|
||||
//
|
||||
// View instance information.
|
||||
//
|
||||
@@ -43,20 +42,55 @@ import (
|
||||
// '200':
|
||||
// description: "Instance information."
|
||||
// schema:
|
||||
// "$ref": "#/definitions/instance"
|
||||
// "$ref": "#/definitions/instanceV1"
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal error
|
||||
func (m *Module) InstanceInformationGETHandler(c *gin.Context) {
|
||||
func (m *Module) InstanceInformationGETHandlerV1(c *gin.Context) {
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
instance, errWithCode := m.processor.InstanceGet(c.Request.Context(), config.GetHost())
|
||||
instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context())
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, instance)
|
||||
}
|
||||
|
||||
// InstanceInformationGETHandlerV2 swagger:operation GET /api/v2/instance instanceGetV2
|
||||
//
|
||||
// View instance information.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - instance
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: "Instance information."
|
||||
// schema:
|
||||
// "$ref": "#/definitions/instanceV2"
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal error
|
||||
func (m *Module) InstanceInformationGETHandlerV2(c *gin.Context) {
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
instance, errWithCode := m.processor.InstanceGetV2(c.Request.Context())
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
|
@@ -130,35 +130,35 @@ import (
|
||||
func (m *Module) InstanceUpdatePATCHHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if !*authed.User.Admin {
|
||||
err := errors.New("user is not an admin so cannot update instance settings")
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
form := &apimodel.InstanceSettingsUpdateRequest{}
|
||||
if err := c.ShouldBind(&form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateInstanceUpdate(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
i, errWithCode := m.processor.InstancePatch(c.Request.Context(), form)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
|
@@ -52,7 +52,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() {
|
||||
|
||||
// set up the request
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx := suite.newContext(recorder, http.MethodPatch, instance.InstanceInformationPath, bodyBytes, w.FormDataContentType(), true)
|
||||
ctx := suite.newContext(recorder, http.MethodPatch, instance.InstanceInformationPathV1, bodyBytes, w.FormDataContentType(), true)
|
||||
|
||||
// call the handler
|
||||
suite.instanceModule.InstanceUpdatePATCHHandler(ctx)
|
||||
@@ -106,7 +106,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() {
|
||||
"max_expiration": 2629746
|
||||
},
|
||||
"accounts": {
|
||||
"allow_custom_css": true
|
||||
"allow_custom_css": true,
|
||||
"max_featured_tags": 10
|
||||
},
|
||||
"emojis": {
|
||||
"emoji_size_limit": 51200
|
||||
@@ -161,7 +162,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() {
|
||||
|
||||
// set up the request
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx := suite.newContext(recorder, http.MethodPatch, instance.InstanceInformationPath, bodyBytes, w.FormDataContentType(), true)
|
||||
ctx := suite.newContext(recorder, http.MethodPatch, instance.InstanceInformationPathV1, bodyBytes, w.FormDataContentType(), true)
|
||||
|
||||
// call the handler
|
||||
suite.instanceModule.InstanceUpdatePATCHHandler(ctx)
|
||||
@@ -215,7 +216,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() {
|
||||
"max_expiration": 2629746
|
||||
},
|
||||
"accounts": {
|
||||
"allow_custom_css": true
|
||||
"allow_custom_css": true,
|
||||
"max_featured_tags": 10
|
||||
},
|
||||
"emojis": {
|
||||
"emoji_size_limit": 51200
|
||||
@@ -270,7 +272,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() {
|
||||
|
||||
// set up the request
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx := suite.newContext(recorder, http.MethodPatch, instance.InstanceInformationPath, bodyBytes, w.FormDataContentType(), true)
|
||||
ctx := suite.newContext(recorder, http.MethodPatch, instance.InstanceInformationPathV1, bodyBytes, w.FormDataContentType(), true)
|
||||
|
||||
// call the handler
|
||||
suite.instanceModule.InstanceUpdatePATCHHandler(ctx)
|
||||
@@ -324,7 +326,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() {
|
||||
"max_expiration": 2629746
|
||||
},
|
||||
"accounts": {
|
||||
"allow_custom_css": true
|
||||
"allow_custom_css": true,
|
||||
"max_featured_tags": 10
|
||||
},
|
||||
"emojis": {
|
||||
"emoji_size_limit": 51200
|
||||
@@ -377,7 +380,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch4() {
|
||||
|
||||
// set up the request
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx := suite.newContext(recorder, http.MethodPatch, instance.InstanceInformationPath, bodyBytes, w.FormDataContentType(), true)
|
||||
ctx := suite.newContext(recorder, http.MethodPatch, instance.InstanceInformationPathV1, bodyBytes, w.FormDataContentType(), true)
|
||||
|
||||
// call the handler
|
||||
suite.instanceModule.InstanceUpdatePATCHHandler(ctx)
|
||||
@@ -406,7 +409,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch5() {
|
||||
|
||||
// set up the request
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx := suite.newContext(recorder, http.MethodPatch, instance.InstanceInformationPath, bodyBytes, w.FormDataContentType(), true)
|
||||
ctx := suite.newContext(recorder, http.MethodPatch, instance.InstanceInformationPathV1, bodyBytes, w.FormDataContentType(), true)
|
||||
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"]))
|
||||
@@ -440,7 +443,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() {
|
||||
|
||||
// set up the request
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx := suite.newContext(recorder, http.MethodPatch, instance.InstanceInformationPath, bodyBytes, w.FormDataContentType(), true)
|
||||
ctx := suite.newContext(recorder, http.MethodPatch, instance.InstanceInformationPathV1, bodyBytes, w.FormDataContentType(), true)
|
||||
|
||||
// call the handler
|
||||
suite.instanceModule.InstanceUpdatePATCHHandler(ctx)
|
||||
@@ -494,7 +497,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() {
|
||||
"max_expiration": 2629746
|
||||
},
|
||||
"accounts": {
|
||||
"allow_custom_css": true
|
||||
"allow_custom_css": true,
|
||||
"max_featured_tags": 10
|
||||
},
|
||||
"emojis": {
|
||||
"emoji_size_limit": 51200
|
||||
@@ -549,7 +553,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch7() {
|
||||
|
||||
// set up the request
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx := suite.newContext(recorder, http.MethodPatch, instance.InstanceInformationPath, bodyBytes, w.FormDataContentType(), true)
|
||||
ctx := suite.newContext(recorder, http.MethodPatch, instance.InstanceInformationPathV1, bodyBytes, w.FormDataContentType(), true)
|
||||
|
||||
// call the handler
|
||||
suite.instanceModule.InstanceUpdatePATCHHandler(ctx)
|
||||
@@ -578,7 +582,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {
|
||||
|
||||
// set up the request
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx := suite.newContext(recorder, http.MethodPatch, instance.InstanceInformationPath, bodyBytes, w.FormDataContentType(), true)
|
||||
ctx := suite.newContext(recorder, http.MethodPatch, instance.InstanceInformationPathV1, bodyBytes, w.FormDataContentType(), true)
|
||||
|
||||
// call the handler
|
||||
suite.instanceModule.InstanceUpdatePATCHHandler(ctx)
|
||||
@@ -636,7 +640,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {
|
||||
"max_expiration": 2629746
|
||||
},
|
||||
"accounts": {
|
||||
"allow_custom_css": true
|
||||
"allow_custom_css": true,
|
||||
"max_featured_tags": 10
|
||||
},
|
||||
"emojis": {
|
||||
"emoji_size_limit": 51200
|
||||
@@ -678,6 +683,24 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {
|
||||
},
|
||||
"max_toot_chars": 5000
|
||||
}`, dst.String())
|
||||
|
||||
// extra bonus: check the v2 model thumbnail after the patch
|
||||
instanceV2, err := suite.processor.InstanceGetV2(ctx)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
instanceV2ThumbnailJson, err := json.MarshalIndent(instanceV2.Thumbnail, "", " ")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Equal(`{
|
||||
"url": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/attachment/original/`+instanceAccount.AvatarMediaAttachment.ID+`.gif",`+`
|
||||
"thumbnail_type": "image/gif",
|
||||
"thumbnail_description": "A bouncing little green peglin.",
|
||||
"blurhash": "LG9t;qRS4YtO.4WDRlt5IXoxtPj["
|
||||
}`, string(instanceV2ThumbnailJson))
|
||||
}
|
||||
|
||||
func TestInstancePatchTestSuite(t *testing.T) {
|
||||
|
@@ -102,14 +102,14 @@ import (
|
||||
func (m *Module) InstancePeersGETHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, false, false, false, false)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
var isUnauthenticated = authed.Account == nil || authed.User == nil
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ func (m *Module) InstancePeersGETHandler(c *gin.Context) {
|
||||
includeOpen = true
|
||||
default:
|
||||
err := fmt.Errorf("filter %s not recognized; accepted values are 'open', 'suspended'", trimmed)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -141,19 +141,19 @@ func (m *Module) InstancePeersGETHandler(c *gin.Context) {
|
||||
|
||||
if includeOpen && !config.GetInstanceExposePeers() && isUnauthenticated {
|
||||
err := fmt.Errorf("peers open query requires an authenticated account/user")
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if includeSuspended && !config.GetInstanceExposeSuspended() && isUnauthenticated {
|
||||
err := fmt.Errorf("peers suspended query requires an authenticated account/user")
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
data, errWithCode := m.processor.InstancePeersGet(c.Request.Context(), includeSuspended, includeOpen, flat)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user