diff --git a/cmd/gotosocial/action/server/server.go b/cmd/gotosocial/action/server/server.go
index f266331f2..dd41f7708 100644
--- a/cmd/gotosocial/action/server/server.go
+++ b/cmd/gotosocial/action/server/server.go
@@ -165,7 +165,7 @@ var Start action.GTSAction = func(ctx context.Context) error {
}
// build client api modules
- authModule := auth.New(dbService, oauthServer, idp)
+ authModule := auth.New(dbService, oauthServer, idp, processor)
accountModule := account.New(processor)
instanceModule := instance.New(processor)
appsModule := app.New(processor)
diff --git a/cmd/gotosocial/action/testrig/testrig.go b/cmd/gotosocial/action/testrig/testrig.go
index cb587c51d..cb0557771 100644
--- a/cmd/gotosocial/action/testrig/testrig.go
+++ b/cmd/gotosocial/action/testrig/testrig.go
@@ -108,7 +108,7 @@ var Start action.GTSAction = func(ctx context.Context) error {
}
// build client api modules
- authModule := auth.New(dbService, oauthServer, idp)
+ authModule := auth.New(dbService, oauthServer, idp, processor)
accountModule := account.New(processor)
instanceModule := instance.New(processor)
appsModule := app.New(processor)
diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml
index f61bcbaea..fa1bd7499 100644
--- a/docs/api/swagger.yaml
+++ b/docs/api/swagger.yaml
@@ -692,7 +692,7 @@ definitions:
text_url:
description: |-
A shorter URL for the attachment.
- Not currently used.
+ In our case, we just give the URL again since we don't create smaller URLs.
type: string
x-go-name: TextURL
type:
@@ -1894,8 +1894,10 @@ paths:
description: unauthorized
"404":
description: not found
+ "406":
+ description: not acceptable
"500":
- description: internal error
+ description: internal server error
security:
- OAuth2 Application:
- write:accounts
@@ -1924,6 +1926,10 @@ paths:
description: unauthorized
"404":
description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- read:accounts
@@ -1952,6 +1958,10 @@ paths:
description: unauthorized
"404":
description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- write:blocks
@@ -1999,6 +2009,10 @@ paths:
description: unauthorized
"404":
description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- write:follows
@@ -2029,6 +2043,10 @@ paths:
description: unauthorized
"404":
description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- read:accounts
@@ -2059,6 +2077,10 @@ paths:
description: unauthorized
"404":
description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- read:accounts
@@ -2134,6 +2156,10 @@ paths:
description: unauthorized
"404":
description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- read:accounts
@@ -2162,6 +2188,10 @@ paths:
description: unauthorized
"404":
description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- write:blocks
@@ -2190,6 +2220,10 @@ paths:
description: unauthorized
"404":
description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- write:follows
@@ -2215,6 +2249,12 @@ paths:
description: bad request
"401":
description: unauthorized
+ "404":
+ description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- write:accounts
@@ -2247,6 +2287,10 @@ paths:
description: unauthorized
"404":
description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- read:accounts
@@ -2313,6 +2357,12 @@ paths:
description: bad request
"401":
description: unauthorized
+ "404":
+ description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- write:accounts
@@ -2335,6 +2385,10 @@ paths:
description: unauthorized
"404":
description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- read:accounts
@@ -2372,6 +2426,12 @@ paths:
description: unauthorized
"403":
description: forbidden
+ "404":
+ description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- admin
@@ -2406,10 +2466,18 @@ paths:
$ref: '#/definitions/emoji'
"400":
description: bad request
+ "401":
+ description: unauthorized
"403":
description: forbidden
+ "404":
+ description: not found
+ "406":
+ description: not acceptable
"409":
description: conflict -- domain/shortcode combo for emoji already exists
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- admin
@@ -2439,10 +2507,16 @@ paths:
type: array
"400":
description: bad request
+ "401":
+ description: unauthorized
"403":
description: forbidden
"404":
description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- admin
@@ -2511,8 +2585,16 @@ paths:
$ref: '#/definitions/domainBlock'
"400":
description: bad request
+ "401":
+ description: unauthorized
"403":
description: forbidden
+ "404":
+ description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- admin
@@ -2537,10 +2619,16 @@ paths:
$ref: '#/definitions/domainBlock'
"400":
description: bad request
+ "401":
+ description: unauthorized
"403":
description: forbidden
"404":
description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- admin
@@ -2564,10 +2652,16 @@ paths:
$ref: '#/definitions/domainBlock'
"400":
description: bad request
+ "401":
+ description: unauthorized
"403":
description: forbidden
"404":
description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- admin
@@ -2599,8 +2693,16 @@ paths:
asynchronously after the request completes.
"400":
description: bad request
+ "401":
+ description: unauthorized
"403":
description: forbidden
+ "404":
+ description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- admin
@@ -2660,10 +2762,14 @@ paths:
description: bad request
"401":
description: unauthorized
- "422":
- description: unprocessable
+ "403":
+ description: forbidden
+ "404":
+ description: not found
+ "406":
+ description: not acceptable
"500":
- description: internal error
+ description: internal server error
summary: Register a new application on this instance.
tags:
- apps
@@ -2714,6 +2820,10 @@ paths:
description: unauthorized
"404":
description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- read:blocks
@@ -2753,10 +2863,12 @@ paths:
description: bad request
"401":
description: unauthorized
- "403":
- description: forbidden
"404":
description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- read:follows
@@ -2785,10 +2897,10 @@ paths:
description: bad request
"401":
description: unauthorized
- "403":
- description: forbidden
"404":
description: not found
+ "406":
+ description: not acceptable
"500":
description: internal server error
security:
@@ -2817,10 +2929,10 @@ paths:
description: bad request
"401":
description: unauthorized
- "403":
- description: forbidden
"404":
description: not found
+ "406":
+ description: not acceptable
"500":
description: internal server error
security:
@@ -2843,6 +2955,8 @@ paths:
description: Instance information.
schema:
$ref: '#/definitions/instance'
+ "406":
+ description: not acceptable
"500":
description: internal error
summary: View instance information.
@@ -2909,6 +3023,14 @@ paths:
description: bad request
"401":
description: unauthorized
+ "403":
+ description: forbidden
+ "404":
+ description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- admin
@@ -2952,10 +3074,10 @@ paths:
description: bad request
"401":
description: unauthorized
- "403":
- description: forbidden
"422":
description: unprocessable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- write:media
@@ -2982,10 +3104,12 @@ paths:
description: bad request
"401":
description: unauthorized
- "403":
- description: forbidden
- "422":
- description: unprocessable
+ "404":
+ description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- read:media
@@ -3036,10 +3160,12 @@ paths:
description: bad request
"401":
description: unauthorized
- "403":
- description: forbidden
- "422":
- description: unprocessable
+ "404":
+ description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- write:media
@@ -3141,6 +3267,12 @@ paths:
description: bad request
"401":
description: unauthorized
+ "404":
+ description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- read:search
@@ -3226,10 +3358,14 @@ paths:
description: bad request
"401":
description: unauthorized
+ "403":
+ description: forbidden
"404":
description: not found
+ "406":
+ description: not acceptable
"500":
- description: internal error
+ description: internal server error
security:
- OAuth2 Bearer:
- write:statuses
@@ -3263,6 +3399,10 @@ paths:
description: forbidden
"404":
description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- write:statuses
@@ -3288,10 +3428,14 @@ paths:
description: bad request
"401":
description: unauthorized
+ "403":
+ description: forbidden
"404":
description: not found
+ "406":
+ description: not acceptable
"500":
- description: internal error
+ description: internal server error
security:
- OAuth2 Bearer:
- read:statuses
@@ -3324,6 +3468,10 @@ paths:
description: forbidden
"404":
description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- read:statuses
@@ -3354,6 +3502,10 @@ paths:
description: forbidden
"404":
description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- write:statuses
@@ -3386,6 +3538,10 @@ paths:
description: forbidden
"404":
description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- read:accounts
@@ -3419,6 +3575,10 @@ paths:
description: forbidden
"404":
description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- write:statuses
@@ -3481,6 +3641,10 @@ paths:
description: forbidden
"404":
description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- write:statuses
@@ -3511,6 +3675,10 @@ paths:
description: forbidden
"404":
description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
security:
- OAuth2 Bearer:
- write:statuses
@@ -3778,6 +3946,8 @@ paths:
description: unauthorized
"403":
description: forbidden
+ "406":
+ description: not acceptable
"500":
description: internal error
security:
diff --git a/internal/api/client/account/accountcreate.go b/internal/api/client/account/accountcreate.go
index 35eb36216..c00d4f1be 100644
--- a/internal/api/client/account/accountcreate.go
+++ b/internal/api/client/account/accountcreate.go
@@ -23,12 +23,11 @@ import (
"net"
"net/http"
- "github.com/sirupsen/logrus"
-
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/validate"
)
@@ -61,58 +60,51 @@ import (
// description: "An OAuth2 access token for the newly-created account."
// schema:
// "$ref": "#/definitions/oauthToken"
-// '401':
-// description: unauthorized
// '400':
// description: bad request
+// '401':
+// description: unauthorized
// '404':
// description: not found
+// '406':
+// description: not acceptable
// '500':
-// description: internal error
+// description: internal server error
func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
- l := logrus.WithField("func", "accountCreatePOSTHandler")
authed, err := oauth.Authed(c, true, true, false, false)
if err != nil {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
- l.Trace("parsing request form")
form := &model.AccountCreateRequest{}
- if err := c.ShouldBind(form); err != nil || form == nil {
- l.Debugf("could not parse form from request: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"})
+ if err := c.ShouldBind(form); err != nil {
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
- l.Tracef("validating form %+v", form)
if err := validateCreateAccount(form); err != nil {
- l.Debugf("error validating form: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
clientIP := c.ClientIP()
- l.Tracef("attempting to parse client ip address %s", clientIP)
signUpIP := net.ParseIP(clientIP)
if signUpIP == nil {
- l.Debugf("error validating sign up ip address %s", clientIP)
- c.JSON(http.StatusBadRequest, gin.H{"error": "ip address could not be parsed from request"})
+ err := errors.New("ip address could not be parsed from request")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
-
form.IP = signUpIP
- ti, err := m.processor.AccountCreate(c.Request.Context(), authed, form)
- if err != nil {
- l.Errorf("internal server error while creating new account: %s", err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ ti, errWithCode := m.processor.AccountCreate(c.Request.Context(), authed, form)
+ if errWithCode != nil {
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
@@ -122,6 +114,10 @@ func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
// validateCreateAccount checks through all the necessary prerequisites for creating a new account,
// according to the provided account create request. If the account isn't eligible, an error will be returned.
func validateCreateAccount(form *model.AccountCreateRequest) error {
+ if form == nil {
+ return errors.New("form was nil")
+ }
+
if !config.GetAccountsRegistrationOpen() {
return errors.New("registration is not open for this server")
}
diff --git a/internal/api/client/account/accountdelete.go b/internal/api/client/account/accountdelete.go
index 2aae2afa0..448eaeb7c 100644
--- a/internal/api/client/account/accountdelete.go
+++ b/internal/api/client/account/accountdelete.go
@@ -19,12 +19,13 @@
package account
import (
+ "errors"
"net/http"
- "github.com/sirupsen/logrus"
-
"github.com/gin-gonic/gin"
+ "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -57,32 +58,35 @@ import (
// description: bad request
// '401':
// description: unauthorized
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) AccountDeletePOSTHandler(c *gin.Context) {
- l := logrus.WithField("func", "AccountDeletePOSTHandler")
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
- l.Tracef("retrieved account %+v", authed.Account.ID)
form := &model.AccountDeleteRequest{}
if err := c.ShouldBind(&form); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
if form.Password == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no password provided in account delete request"})
+ err = errors.New("no password provided in account delete request")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
form.DeleteOriginID = authed.Account.ID
if errWithCode := m.processor.AccountDeleteLocal(c.Request.Context(), authed, form); errWithCode != nil {
- l.Debugf("could not delete account: %s", errWithCode.Error())
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/account/accountget.go b/internal/api/client/account/accountget.go
index 1fa7014eb..2a060e175 100644
--- a/internal/api/client/account/accountget.go
+++ b/internal/api/client/account/accountget.go
@@ -19,11 +19,12 @@
package account
import (
+ "errors"
"net/http"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -53,34 +54,38 @@ import (
// '200':
// schema:
// "$ref": "#/definitions/account"
-// '401':
-// description: unauthorized
// '400':
// description: bad request
+// '401':
+// description: unauthorized
// '404':
// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) AccountGETHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
targetAcctID := c.Param(IDKey)
if targetAcctID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
+ err := errors.New("no account id specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
acctInfo, errWithCode := m.processor.AccountGet(c.Request.Context(), authed, targetAcctID)
- if err != nil {
- logrus.Debug(errWithCode.Error())
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ if errWithCode != nil {
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/account/accountupdate.go b/internal/api/client/account/accountupdate.go
index 0317fbac0..7fae681d2 100644
--- a/internal/api/client/account/accountupdate.go
+++ b/internal/api/client/account/accountupdate.go
@@ -19,15 +19,15 @@
package account
import (
+ "errors"
"fmt"
"net/http"
"strconv"
- "github.com/sirupsen/logrus"
-
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -98,68 +98,67 @@ import (
// description: "The newly updated account."
// schema:
// "$ref": "#/definitions/account"
-// '401':
-// description: unauthorized
// '400':
// description: bad request
+// '401':
+// description: unauthorized
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) AccountUpdateCredentialsPATCHHandler(c *gin.Context) {
- l := logrus.WithField("func", "accountUpdateCredentialsPATCHHandler")
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
- l.Tracef("retrieved account %+v", authed.Account.ID)
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
form, err := parseUpdateAccountForm(c)
if err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
- // if everything on the form is nil, then nothing has been set and we shouldn't continue
- if form.Discoverable == nil &&
- form.Bot == nil &&
- form.DisplayName == nil &&
- form.Note == nil &&
- form.Avatar == nil &&
- form.Header == nil &&
- form.Locked == nil &&
- form.Source.Privacy == nil &&
- form.Source.Sensitive == nil &&
- form.Source.Language == nil &&
- form.FieldsAttributes == nil {
- l.Debugf("could not parse form from request")
- c.JSON(http.StatusBadRequest, gin.H{"error": "empty form submitted"})
+ acctSensitive, errWithCode := m.processor.AccountUpdate(c.Request.Context(), authed, form)
+ if errWithCode != nil {
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
- acctSensitive, err := m.processor.AccountUpdate(c.Request.Context(), authed, form)
- if err != nil {
- l.Debugf("could not update account: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
-
- l.Tracef("conversion successful, returning OK and apisensitive account %+v", acctSensitive)
c.JSON(http.StatusOK, acctSensitive)
}
func parseUpdateAccountForm(c *gin.Context) (*model.UpdateCredentialsRequest, error) {
- // parse main fields from request
form := &model.UpdateCredentialsRequest{
Source: &model.UpdateSource{},
}
- if err := c.ShouldBind(&form); err != nil || form == nil {
+
+ if err := c.ShouldBind(&form); err != nil {
return nil, fmt.Errorf("could not parse form from request: %s", err)
}
+ if form == nil ||
+ (form.Discoverable == nil &&
+ form.Bot == nil &&
+ form.DisplayName == nil &&
+ form.Note == nil &&
+ form.Avatar == nil &&
+ form.Header == nil &&
+ form.Locked == nil &&
+ form.Source.Privacy == nil &&
+ form.Source.Sensitive == nil &&
+ form.Source.Language == nil &&
+ form.FieldsAttributes == nil) {
+ return nil, errors.New("empty form submitted")
+ }
+
// parse source field-by-field
sourceMap := c.PostFormMap("source")
diff --git a/internal/api/client/account/accountupdate_test.go b/internal/api/client/account/accountupdate_test.go
index c6d07d9dc..91f886721 100644
--- a/internal/api/client/account/accountupdate_test.go
+++ b/internal/api/client/account/accountupdate_test.go
@@ -26,7 +26,6 @@ import (
"net/http/httptest"
"testing"
- "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/api/client/account"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
@@ -65,7 +64,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandler()
// check the response
b, err := ioutil.ReadAll(result.Body)
- assert.NoError(suite.T(), err)
+ suite.NoError(err)
// unmarshal the returned account
apimodelAccount := &apimodel.Account{}
@@ -104,7 +103,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUnl
// check the response
b1, err := ioutil.ReadAll(result1.Body)
- assert.NoError(suite.T(), err)
+ suite.NoError(err)
// unmarshal the returned account
apimodelAccount1 := &apimodel.Account{}
@@ -185,7 +184,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerGet
// check the response
b, err := ioutil.ReadAll(result.Body)
- assert.NoError(suite.T(), err)
+ suite.NoError(err)
// unmarshal the returned account
apimodelAccount := &apimodel.Account{}
@@ -227,7 +226,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerTwo
// check the response
b, err := ioutil.ReadAll(result.Body)
- assert.NoError(suite.T(), err)
+ suite.NoError(err)
// unmarshal the returned account
apimodelAccount := &apimodel.Account{}
@@ -271,7 +270,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerWit
// check the response
b, err := ioutil.ReadAll(result.Body)
- assert.NoError(suite.T(), err)
+ suite.NoError(err)
// unmarshal the returned account
apimodelAccount := &apimodel.Account{}
@@ -313,8 +312,8 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerEmp
// check the response
b, err := ioutil.ReadAll(result.Body)
- assert.NoError(suite.T(), err)
- suite.Equal(`{"error":"empty form submitted"}`, string(b))
+ suite.NoError(err)
+ suite.Equal(`{"error":"Bad Request: empty form submitted"}`, string(b))
}
func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpdateSource() {
@@ -348,7 +347,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpd
// check the response
b, err := ioutil.ReadAll(result.Body)
- assert.NoError(suite.T(), err)
+ suite.NoError(err)
// unmarshal the returned account
apimodelAccount := &apimodel.Account{}
diff --git a/internal/api/client/account/accountverify.go b/internal/api/client/account/accountverify.go
index f3d0643cc..e6cb65efd 100644
--- a/internal/api/client/account/accountverify.go
+++ b/internal/api/client/account/accountverify.go
@@ -21,10 +21,9 @@ package account
import (
"net/http"
- "github.com/sirupsen/logrus"
-
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -47,30 +46,31 @@ import (
// '200':
// schema:
// "$ref": "#/definitions/account"
-// '401':
-// description: unauthorized
// '400':
// description: bad request
+// '401':
+// description: unauthorized
// '404':
// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) AccountVerifyGETHandler(c *gin.Context) {
- l := logrus.WithField("func", "accountVerifyGETHandler")
- authed, err := oauth.Authed(c, true, false, false, true)
+ authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
- acctSensitive, err := m.processor.AccountGet(c.Request.Context(), authed, authed.Account.ID)
- if err != nil {
- l.Debugf("error getting account from processor: %s", err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
+ acctSensitive, errWithCode := m.processor.AccountGet(c.Request.Context(), authed, authed.Account.ID)
+ if errWithCode != nil {
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/account/block.go b/internal/api/client/account/block.go
index ed602aefc..b5f7fdda8 100644
--- a/internal/api/client/account/block.go
+++ b/internal/api/client/account/block.go
@@ -19,10 +19,12 @@
package account
import (
+ "errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -54,33 +56,38 @@ import (
// description: Your relationship to this account.
// schema:
// "$ref": "#/definitions/accountRelationship"
-// '401':
-// description: unauthorized
// '400':
// description: bad request
+// '401':
+// description: unauthorized
// '404':
// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) AccountBlockPOSTHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
targetAcctID := c.Param(IDKey)
if targetAcctID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
+ err := errors.New("no account id specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
relationship, errWithCode := m.processor.AccountBlockCreate(c.Request.Context(), authed, targetAcctID)
if errWithCode != nil {
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/account/follow.go b/internal/api/client/account/follow.go
index b17dd9636..11bfbf965 100644
--- a/internal/api/client/account/follow.go
+++ b/internal/api/client/account/follow.go
@@ -19,11 +19,13 @@
package account
import (
+ "errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -75,39 +77,45 @@ import (
// description: Your relationship to this account.
// schema:
// "$ref": "#/definitions/accountRelationship"
-// '401':
-// description: unauthorized
// '400':
// description: bad request
+// '401':
+// description: unauthorized
// '404':
// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) AccountFollowPOSTHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
targetAcctID := c.Param(IDKey)
if targetAcctID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
+ err := errors.New("no account id specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
+
form := &model.AccountFollowRequest{}
if err := c.ShouldBind(form); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
form.ID = targetAcctID
relationship, errWithCode := m.processor.AccountFollowCreate(c.Request.Context(), authed, form)
if errWithCode != nil {
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/account/followers.go b/internal/api/client/account/followers.go
index 9addb2ca7..beb82a34e 100644
--- a/internal/api/client/account/followers.go
+++ b/internal/api/client/account/followers.go
@@ -19,10 +19,12 @@
package account
import (
+ "errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -56,33 +58,38 @@ import (
// type: array
// items:
// "$ref": "#/definitions/account"
-// '401':
-// description: unauthorized
// '400':
// description: bad request
+// '401':
+// description: unauthorized
// '404':
// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) AccountFollowersGETHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
targetAcctID := c.Param(IDKey)
if targetAcctID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
+ err := errors.New("no account id specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
followers, errWithCode := m.processor.AccountFollowersGet(c.Request.Context(), authed, targetAcctID)
if errWithCode != nil {
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/account/following.go b/internal/api/client/account/following.go
index 6a857f43d..d03bf143d 100644
--- a/internal/api/client/account/following.go
+++ b/internal/api/client/account/following.go
@@ -19,10 +19,12 @@
package account
import (
+ "errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -56,33 +58,38 @@ import (
// type: array
// items:
// "$ref": "#/definitions/account"
-// '401':
-// description: unauthorized
// '400':
// description: bad request
+// '401':
+// description: unauthorized
// '404':
// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) AccountFollowingGETHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
targetAcctID := c.Param(IDKey)
if targetAcctID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
+ err := errors.New("no account id specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
following, errWithCode := m.processor.AccountFollowingGet(c.Request.Context(), authed, targetAcctID)
if errWithCode != nil {
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/account/relationships.go b/internal/api/client/account/relationships.go
index 22ae835f0..8facc118c 100644
--- a/internal/api/client/account/relationships.go
+++ b/internal/api/client/account/relationships.go
@@ -1,13 +1,13 @@
package account
import (
+ "errors"
"net/http"
- "github.com/sirupsen/logrus"
-
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -43,24 +43,25 @@ import (
// type: array
// items:
// "$ref": "#/definitions/accountRelationship"
-// '401':
-// description: unauthorized
// '400':
// description: bad request
+// '401':
+// description: unauthorized
// '404':
// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) {
- l := logrus.WithField("func", "AccountRelationshipsGETHandler")
-
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debugf("error authing: %s", err)
- c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
@@ -69,8 +70,8 @@ func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) {
// check fallback -- let's be generous and see if maybe it's just set as 'id'?
id := c.Query("id")
if id == "" {
- l.Debug("no account id specified in query")
- c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
+ err = errors.New("no account id(s) specified in query")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
targetAccountIDs = append(targetAccountIDs, id)
@@ -80,8 +81,8 @@ func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) {
for _, targetAccountID := range targetAccountIDs {
r, errWithCode := m.processor.AccountRelationshipGet(c.Request.Context(), authed, targetAccountID)
- if err != nil {
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ if errWithCode != nil {
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
relationships = append(relationships, *r)
diff --git a/internal/api/client/account/statuses.go b/internal/api/client/account/statuses.go
index 18b551fcc..2f0b804d3 100644
--- a/internal/api/client/account/statuses.go
+++ b/internal/api/client/account/statuses.go
@@ -19,13 +19,14 @@
package account
import (
+ "errors"
+ "fmt"
"net/http"
"strconv"
- "github.com/sirupsen/logrus"
-
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -110,31 +111,32 @@ import (
// type: array
// items:
// "$ref": "#/definitions/status"
-// '401':
-// description: unauthorized
// '400':
// description: bad request
+// '401':
+// description: unauthorized
// '404':
// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
- l := logrus.WithField("func", "AccountStatusesGETHandler")
-
authed, err := oauth.Authed(c, false, false, false, false)
if err != nil {
- l.Debugf("error authing: %s", err)
- c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
targetAcctID := c.Param(IDKey)
if targetAcctID == "" {
- l.Debug("no account id specified in query")
- c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
+ err := errors.New("no account id specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
@@ -143,8 +145,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
if limitString != "" {
i, err := strconv.ParseInt(limitString, 10, 64)
if err != nil {
- l.Debugf("error parsing limit string: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"})
+ err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
limit = int(i)
@@ -155,8 +157,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
if excludeRepliesString != "" {
i, err := strconv.ParseBool(excludeRepliesString)
if err != nil {
- l.Debugf("error parsing exclude replies string: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse exclude replies query param"})
+ err := fmt.Errorf("error parsing %s: %s", ExcludeRepliesKey, err)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
excludeReplies = i
@@ -167,8 +169,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
if excludeReblogsString != "" {
i, err := strconv.ParseBool(excludeReblogsString)
if err != nil {
- l.Debugf("error parsing exclude reblogs string: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse exclude reblogs query param"})
+ err := fmt.Errorf("error parsing %s: %s", ExcludeReblogsKey, err)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
excludeReblogs = i
@@ -191,8 +193,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
if pinnedString != "" {
i, err := strconv.ParseBool(pinnedString)
if err != nil {
- l.Debugf("error parsing pinned string: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse pinned query param"})
+ err := fmt.Errorf("error parsing %s: %s", PinnedKey, err)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
pinnedOnly = i
@@ -203,8 +205,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
if mediaOnlyString != "" {
i, err := strconv.ParseBool(mediaOnlyString)
if err != nil {
- l.Debugf("error parsing media only string: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse media only query param"})
+ err := fmt.Errorf("error parsing %s: %s", OnlyMediaKey, err)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
mediaOnly = i
@@ -215,8 +217,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
if publicOnlyString != "" {
i, err := strconv.ParseBool(publicOnlyString)
if err != nil {
- l.Debugf("error parsing public only string: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse public only query param"})
+ err := fmt.Errorf("error parsing %s: %s", OnlyPublicKey, err)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
publicOnly = i
@@ -224,8 +226,7 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
resp, errWithCode := m.processor.AccountStatusesGet(c.Request.Context(), authed, targetAcctID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly)
if errWithCode != nil {
- l.Debugf("error from processor account statuses get: %s", errWithCode)
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/account/unblock.go b/internal/api/client/account/unblock.go
index a8ae0cf21..44f3a722c 100644
--- a/internal/api/client/account/unblock.go
+++ b/internal/api/client/account/unblock.go
@@ -19,10 +19,12 @@
package account
import (
+ "errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -54,33 +56,38 @@ import (
// description: Your relationship to this account.
// schema:
// "$ref": "#/definitions/accountRelationship"
-// '401':
-// description: unauthorized
// '400':
// description: bad request
+// '401':
+// description: unauthorized
// '404':
// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) AccountUnblockPOSTHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
targetAcctID := c.Param(IDKey)
if targetAcctID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
+ err := errors.New("no account id specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
relationship, errWithCode := m.processor.AccountBlockRemove(c.Request.Context(), authed, targetAcctID)
if errWithCode != nil {
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/account/unfollow.go b/internal/api/client/account/unfollow.go
index e681e2773..d92278f95 100644
--- a/internal/api/client/account/unfollow.go
+++ b/internal/api/client/account/unfollow.go
@@ -19,12 +19,12 @@
package account
import (
+ "errors"
"net/http"
- "github.com/sirupsen/logrus"
-
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -56,37 +56,38 @@ import (
// description: Your relationship to this account.
// schema:
// "$ref": "#/definitions/accountRelationship"
-// '401':
-// description: unauthorized
// '400':
// description: bad request
+// '401':
+// description: unauthorized
// '404':
// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) AccountUnfollowPOSTHandler(c *gin.Context) {
- l := logrus.WithField("func", "AccountUnfollowPOSTHandler")
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debug(err)
- c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
targetAcctID := c.Param(IDKey)
if targetAcctID == "" {
- l.Debug(err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
+ err := errors.New("no account id specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
relationship, errWithCode := m.processor.AccountFollowRemove(c.Request.Context(), authed, targetAcctID)
if errWithCode != nil {
- l.Debug(errWithCode.Error())
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/admin/accountaction.go b/internal/api/client/admin/accountaction.go
index 46473fa73..072a60a9d 100644
--- a/internal/api/client/admin/accountaction.go
+++ b/internal/api/client/admin/accountaction.go
@@ -19,12 +19,14 @@
package admin
import (
+ "errors"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
+ "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -72,53 +74,47 @@ import (
// description: unauthorized
// '403':
// description: forbidden
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) AccountActionPOSTHandler(c *gin.Context) {
- l := logrus.WithFields(logrus.Fields{
- "func": "AccountActionPOSTHandler",
- "request_uri": c.Request.RequestURI,
- "user_agent": c.Request.UserAgent(),
- "origin_ip": c.ClientIP(),
- })
-
- // make sure we're authed...
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
- // with an admin account
if !authed.User.Admin {
- l.Debugf("user %s not an admin", authed.User.ID)
- c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"})
+ err := fmt.Errorf("user %s not an admin", authed.User.ID)
+ api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
return
}
- // extract the form from the request context
- l.Tracef("parsing request form: %+v", c.Request.Form)
form := &model.AdminAccountActionRequest{}
if err := c.ShouldBind(form); err != nil {
- l.Debugf("error parsing form %+v: %s", c.Request.Form, err)
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not parse form: %s", err)})
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
if form.Type == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no type specified"})
+ err := errors.New("no type specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
targetAcctID := c.Param(IDKey)
if targetAcctID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
+ err := errors.New("no account id specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
form.TargetAccountID = targetAcctID
if errWithCode := m.processor.AdminAccountAction(c.Request.Context(), authed, form); errWithCode != nil {
- l.Debugf("error performing account action: %s", errWithCode.Error())
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/admin/domainblockcreate.go b/internal/api/client/admin/domainblockcreate.go
index dd5623a1c..e23376a91 100644
--- a/internal/api/client/admin/domainblockcreate.go
+++ b/internal/api/client/admin/domainblockcreate.go
@@ -7,9 +7,9 @@ import (
"strconv"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -86,33 +86,33 @@ import (
// Note that if a list has been imported, then an `array` of newly created domain blocks will be returned instead.
// schema:
// "$ref": "#/definitions/domainBlock"
-// '403':
-// description: forbidden
// '400':
// description: bad request
+// '401':
+// description: unauthorized
+// '403':
+// description: forbidden
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) {
- l := logrus.WithFields(logrus.Fields{
- "func": "DomainBlocksPOSTHandler",
- "request_uri": c.Request.RequestURI,
- "user_agent": c.Request.UserAgent(),
- "origin_ip": c.ClientIP(),
- })
-
- // make sure we're authed with an admin account
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
+
if !authed.User.Admin {
- l.Debugf("user %s not an admin", authed.User.ID)
- c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"})
+ err := fmt.Errorf("user %s not an admin", authed.User.ID)
+ api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
@@ -121,49 +121,43 @@ func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) {
if importString != "" {
i, err := strconv.ParseBool(importString)
if err != nil {
- l.Debugf("error parsing import string: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse import query param"})
+ err := fmt.Errorf("error parsing %s: %s", ImportQueryKey, err)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
imp = i
}
- // extract the media create form from the request context
- l.Tracef("parsing request form: %+v", c.Request.Form)
form := &model.DomainBlockCreateRequest{}
if err := c.ShouldBind(form); err != nil {
- l.Debugf("error parsing form %+v: %s", c.Request.Form, err)
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not parse form: %s", err)})
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
- // Give the fields on the request form a first pass to make sure the request is superficially valid.
- l.Tracef("validating form %+v", form)
if err := validateCreateDomainBlock(form, imp); err != nil {
- l.Debugf("error validating form: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ err := fmt.Errorf("error validating form: %s", err)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
if imp {
// we're importing multiple blocks
- domainBlocks, err := m.processor.AdminDomainBlocksImport(c.Request.Context(), authed, form)
- if err != nil {
- l.Debugf("error importing domain blocks: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ domainBlocks, errWithCode := m.processor.AdminDomainBlocksImport(c.Request.Context(), authed, form)
+ if errWithCode != nil {
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
c.JSON(http.StatusOK, domainBlocks)
- } else {
- // we're just creating one block
- domainBlock, err := m.processor.AdminDomainBlockCreate(c.Request.Context(), authed, form)
- if err != nil {
- l.Debugf("error creating domain block: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
- c.JSON(http.StatusOK, domainBlock)
+ return
}
+
+ // we're just creating one block
+ domainBlock, errWithCode := m.processor.AdminDomainBlockCreate(c.Request.Context(), authed, form)
+ if errWithCode != nil {
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
+ return
+ }
+ c.JSON(http.StatusOK, domainBlock)
}
func validateCreateDomainBlock(form *model.DomainBlockCreateRequest, imp bool) error {
diff --git a/internal/api/client/admin/domainblockdelete.go b/internal/api/client/admin/domainblockdelete.go
index 8d41ec072..416db002b 100644
--- a/internal/api/client/admin/domainblockdelete.go
+++ b/internal/api/client/admin/domainblockdelete.go
@@ -1,11 +1,13 @@
package admin
import (
+ "errors"
+ "fmt"
"net/http"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -36,48 +38,46 @@ import (
// description: The domain block that was just deleted.
// schema:
// "$ref": "#/definitions/domainBlock"
-// '403':
-// description: forbidden
// '400':
// description: bad request
+// '401':
+// description: unauthorized
+// '403':
+// description: forbidden
// '404':
// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) DomainBlockDELETEHandler(c *gin.Context) {
- l := logrus.WithFields(logrus.Fields{
- "func": "DomainBlockDELETEHandler",
- "request_uri": c.Request.RequestURI,
- "user_agent": c.Request.UserAgent(),
- "origin_ip": c.ClientIP(),
- })
-
- // make sure we're authed with an admin account
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
+
if !authed.User.Admin {
- l.Debugf("user %s not an admin", authed.User.ID)
- c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"})
+ err := fmt.Errorf("user %s not an admin", authed.User.ID)
+ api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
domainBlockID := c.Param(IDKey)
if domainBlockID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no domain block id provided"})
+ err := errors.New("no domain block id specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
domainBlock, errWithCode := m.processor.AdminDomainBlockDelete(c.Request.Context(), authed, domainBlockID)
if errWithCode != nil {
- l.Debugf("error deleting domain block: %s", errWithCode.Error())
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/admin/domainblockget.go b/internal/api/client/admin/domainblockget.go
index 49a0795d7..15456242c 100644
--- a/internal/api/client/admin/domainblockget.go
+++ b/internal/api/client/admin/domainblockget.go
@@ -1,12 +1,14 @@
package admin
import (
+ "errors"
+ "fmt"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -37,41 +39,40 @@ import (
// description: The requested domain block.
// schema:
// "$ref": "#/definitions/domainBlock"
-// '403':
-// description: forbidden
// '400':
// description: bad request
+// '401':
+// description: unauthorized
+// '403':
+// description: forbidden
// '404':
// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) DomainBlockGETHandler(c *gin.Context) {
- l := logrus.WithFields(logrus.Fields{
- "func": "DomainBlockGETHandler",
- "request_uri": c.Request.RequestURI,
- "user_agent": c.Request.UserAgent(),
- "origin_ip": c.ClientIP(),
- })
-
- // make sure we're authed with an admin account
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
+
if !authed.User.Admin {
- l.Debugf("user %s not an admin", authed.User.ID)
- c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"})
+ err := fmt.Errorf("user %s not an admin", authed.User.ID)
+ api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
domainBlockID := c.Param(IDKey)
if domainBlockID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no domain block id provided"})
+ err := errors.New("no domain block id specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
@@ -80,17 +81,16 @@ func (m *Module) DomainBlockGETHandler(c *gin.Context) {
if exportString != "" {
i, err := strconv.ParseBool(exportString)
if err != nil {
- l.Debugf("error parsing export string: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse export query param"})
+ err := fmt.Errorf("error parsing %s: %s", ExportQueryKey, err)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
export = i
}
- domainBlock, err := m.processor.AdminDomainBlockGet(c.Request.Context(), authed, domainBlockID, export)
- if err != nil {
- l.Debugf("error getting domain block: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ domainBlock, errWithCode := m.processor.AdminDomainBlockGet(c.Request.Context(), authed, domainBlockID, export)
+ if errWithCode != nil {
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/admin/domainblocksget.go b/internal/api/client/admin/domainblocksget.go
index 94b1c89ce..d585e837d 100644
--- a/internal/api/client/admin/domainblocksget.go
+++ b/internal/api/client/admin/domainblocksget.go
@@ -1,12 +1,14 @@
package admin
import (
+ "errors"
+ "fmt"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -43,35 +45,40 @@ import (
// type: array
// items:
// "$ref": "#/definitions/domainBlock"
-// '403':
-// description: forbidden
// '400':
// description: bad request
+// '401':
+// description: unauthorized
+// '403':
+// description: forbidden
// '404':
// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) DomainBlocksGETHandler(c *gin.Context) {
- l := logrus.WithFields(logrus.Fields{
- "func": "DomainBlocksGETHandler",
- "request_uri": c.Request.RequestURI,
- "user_agent": c.Request.UserAgent(),
- "origin_ip": c.ClientIP(),
- })
-
- // make sure we're authed with an admin account
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
+
if !authed.User.Admin {
- l.Debugf("user %s not an admin", authed.User.ID)
- c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"})
+ err := fmt.Errorf("user %s not an admin", authed.User.ID)
+ api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
+ return
+ }
+
+ domainBlockID := c.Param(IDKey)
+ if domainBlockID == "" {
+ err := errors.New("no domain block id specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
@@ -80,17 +87,16 @@ func (m *Module) DomainBlocksGETHandler(c *gin.Context) {
if exportString != "" {
i, err := strconv.ParseBool(exportString)
if err != nil {
- l.Debugf("error parsing export string: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse export query param"})
+ err := fmt.Errorf("error parsing %s: %s", ExportQueryKey, err)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
export = i
}
- domainBlocks, err := m.processor.AdminDomainBlocksGet(c.Request.Context(), authed, export)
- if err != nil {
- l.Debugf("error getting domain blocks: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ domainBlocks, errWithCode := m.processor.AdminDomainBlocksGet(c.Request.Context(), authed, export)
+ if errWithCode != nil {
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/admin/emojicreate.go b/internal/api/client/admin/emojicreate.go
index ef42d0a13..c5d613312 100644
--- a/internal/api/client/admin/emojicreate.go
+++ b/internal/api/client/admin/emojicreate.go
@@ -24,9 +24,9 @@ import (
"net/http"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/validate"
)
@@ -69,59 +69,52 @@ import (
// description: The newly-created emoji.
// schema:
// "$ref": "#/definitions/emoji"
-// '403':
-// description: forbidden
// '400':
// description: bad request
+// '401':
+// description: unauthorized
+// '403':
+// description: forbidden
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
// '409':
// description: conflict -- domain/shortcode combo for emoji already exists
+// '500':
+// description: internal server error
func (m *Module) EmojiCreatePOSTHandler(c *gin.Context) {
- l := logrus.WithFields(logrus.Fields{
- "func": "emojiCreatePOSTHandler",
- "request_uri": c.Request.RequestURI,
- "user_agent": c.Request.UserAgent(),
- "origin_ip": c.ClientIP(),
- })
-
- // make sure we're authed with an admin account
- authed, err := oauth.Authed(c, true, true, true, true) // posting a status is serious business so we want *everything*
+ authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
+
if !authed.User.Admin {
- l.Debugf("user %s not an admin", authed.User.ID)
- c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"})
+ err := fmt.Errorf("user %s not an admin", authed.User.ID)
+ api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
- // extract the media create form from the request context
- l.Tracef("parsing request form: %+v", c.Request.Form)
form := &model.EmojiCreateRequest{}
if err := c.ShouldBind(form); err != nil {
- l.Debugf("error parsing form %+v: %s", c.Request.Form, err)
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not parse form: %s", err)})
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
- // Give the fields on the request form a first pass to make sure the request is superficially valid.
- l.Tracef("validating form %+v", form)
if err := validateCreateEmoji(form); err != nil {
- l.Debugf("error validating form: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
apiEmoji, errWithCode := m.processor.AdminEmojiCreate(c.Request.Context(), authed, form)
if errWithCode != nil {
- l.Debugf("error creating emoji: %s", errWithCode.Error())
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
@@ -129,7 +122,6 @@ func (m *Module) EmojiCreatePOSTHandler(c *gin.Context) {
}
func validateCreateEmoji(form *model.EmojiCreateRequest) error {
- // check there actually is an image attached and it's not size 0
if form.Image == nil || form.Image.Size == 0 {
return errors.New("no emoji given")
}
diff --git a/internal/api/client/admin/emojicreate_test.go b/internal/api/client/admin/emojicreate_test.go
index 2b7476da1..1b6ddf96f 100644
--- a/internal/api/client/admin/emojicreate_test.go
+++ b/internal/api/client/admin/emojicreate_test.go
@@ -120,7 +120,7 @@ func (suite *EmojiCreateTestSuite) TestEmojiCreateAlreadyExists() {
suite.NoError(err)
suite.NotEmpty(b)
- suite.Equal(`{"error":"conflict: emoji with shortcode rainbow already exists"}`, string(b))
+ suite.Equal(`{"error":"Conflict: emoji with shortcode rainbow already exists"}`, string(b))
}
func TestEmojiCreateTestSuite(t *testing.T) {
diff --git a/internal/api/client/admin/mediacleanup.go b/internal/api/client/admin/mediacleanup.go
index 02eec82f3..6065acd3b 100644
--- a/internal/api/client/admin/mediacleanup.go
+++ b/internal/api/client/admin/mediacleanup.go
@@ -23,9 +23,10 @@ import (
"net/http"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
+ "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -54,39 +55,34 @@ import (
// '200':
// description: |-
// Echos the number of days requested. The cleanup is performed asynchronously after the request completes.
-// '403':
-// description: forbidden
// '400':
// description: bad request
+// '401':
+// description: unauthorized
+// '403':
+// description: forbidden
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) MediaCleanupPOSTHandler(c *gin.Context) {
- l := logrus.WithFields(logrus.Fields{
- "func": "MediaCleanupPOSTHandler",
- "request_uri": c.Request.RequestURI,
- "user_agent": c.Request.UserAgent(),
- "origin_ip": c.ClientIP(),
- })
-
- // make sure we're authed...
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
- // with an admin account
if !authed.User.Admin {
- l.Debugf("user %s not an admin", authed.User.ID)
- c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"})
+ err := fmt.Errorf("user %s not an admin", authed.User.ID)
+ api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
return
}
- // extract the form from the request context
- l.Tracef("parsing request form: %+v", c.Request.Form)
form := &model.MediaCleanupRequest{}
if err := c.ShouldBind(form); err != nil {
- l.Debugf("error parsing form %+v: %s", c.Request.Form, err)
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not parse form: %s", err)})
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
@@ -101,8 +97,7 @@ func (m *Module) MediaCleanupPOSTHandler(c *gin.Context) {
}
if errWithCode := m.processor.AdminMediaPrune(c.Request.Context(), remoteCacheDays); errWithCode != nil {
- l.Debugf("error starting prune of remote media: %s", errWithCode.Error())
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/app/appcreate.go b/internal/api/client/app/appcreate.go
index 2e101547c..d402628de 100644
--- a/internal/api/client/app/appcreate.go
+++ b/internal/api/client/app/appcreate.go
@@ -22,18 +22,16 @@ import (
"fmt"
"net/http"
- "github.com/sirupsen/logrus"
-
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
+// these consts are used to ensure users can't spam huge entries into our database
const (
- // permitted length for most fields
- formFieldLen = 64
- // redirect can be a bit bigger because we probably need to encode data in the redirect uri
+ formFieldLen = 64
formRedirectLen = 512
)
@@ -64,56 +62,63 @@ const (
// description: "The newly-created application."
// schema:
// "$ref": "#/definitions/application"
-// '401':
-// description: unauthorized
// '400':
// description: bad request
-// '422':
-// description: unprocessable
+// '401':
+// description: unauthorized
+// '403':
+// description: forbidden
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
// '500':
-// description: internal error
+// description: internal server error
func (m *Module) AppsPOSTHandler(c *gin.Context) {
- l := logrus.WithField("func", "AppsPOSTHandler")
- l.Trace("entering AppsPOSTHandler")
-
authed, err := oauth.Authed(c, false, false, false, false)
if err != nil {
- c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
form := &model.ApplicationCreateRequest{}
if err := c.ShouldBind(form); err != nil {
- c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
- // check lengths of fields before proceeding so the user can't spam huge entries into the database
if len(form.ClientName) > formFieldLen {
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("client_name must be less than %d bytes", formFieldLen)})
- return
- }
- if len(form.Website) > formFieldLen {
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("website must be less than %d bytes", formFieldLen)})
- return
- }
- if len(form.RedirectURIs) > formRedirectLen {
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("redirect_uris must be less than %d bytes", formRedirectLen)})
- return
- }
- if len(form.Scopes) > formFieldLen {
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("scopes must be less than %d bytes", formFieldLen)})
+ err := fmt.Errorf("client_name must be less than %d bytes", formFieldLen)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
- apiApp, err := m.processor.AppCreate(c.Request.Context(), authed, form)
- if err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ if len(form.RedirectURIs) > formRedirectLen {
+ err := fmt.Errorf("redirect_uris must be less than %d bytes", formRedirectLen)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
+ return
+ }
+
+ if len(form.Scopes) > formFieldLen {
+ err := fmt.Errorf("scopes must be less than %d bytes", formFieldLen)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
+ return
+ }
+
+ if len(form.Website) > formFieldLen {
+ err := fmt.Errorf("website must be less than %d bytes", formFieldLen)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
+ return
+ }
+
+ apiApp, errWithCode := m.processor.AppCreate(c.Request.Context(), authed, form)
+ if errWithCode != nil {
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/auth/auth.go b/internal/api/client/auth/auth.go
index 717d997a3..10d374838 100644
--- a/internal/api/client/auth/auth.go
+++ b/internal/api/client/auth/auth.go
@@ -25,6 +25,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/oidc"
+ "github.com/superseriousbusiness/gotosocial/internal/processing"
"github.com/superseriousbusiness/gotosocial/internal/router"
)
@@ -66,17 +67,19 @@ const (
// Module implements the ClientAPIModule interface for
type Module struct {
- db db.DB
- server oauth.Server
- idp oidc.IDP
+ db db.DB
+ server oauth.Server
+ idp oidc.IDP
+ processor processing.Processor
}
// New returns a new auth module
-func New(db db.DB, server oauth.Server, idp oidc.IDP) api.ClientModule {
+func New(db db.DB, server oauth.Server, idp oidc.IDP, processor processing.Processor) api.ClientModule {
return &Module{
- db: db,
- server: server,
- idp: idp,
+ db: db,
+ server: server,
+ idp: idp,
+ processor: processor,
}
}
diff --git a/internal/api/client/auth/auth_test.go b/internal/api/client/auth/auth_test.go
index bd1a1948e..f222f714f 100644
--- a/internal/api/client/auth/auth_test.go
+++ b/internal/api/client/auth/auth_test.go
@@ -23,25 +23,37 @@ import (
"fmt"
"net/http/httptest"
+ "codeberg.org/gruf/go-store/kv"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/memstore"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/api/client/auth"
+ "github.com/superseriousbusiness/gotosocial/internal/concurrency"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/email"
+ "github.com/superseriousbusiness/gotosocial/internal/federation"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/media"
+ "github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/oidc"
+ "github.com/superseriousbusiness/gotosocial/internal/processing"
"github.com/superseriousbusiness/gotosocial/internal/router"
"github.com/superseriousbusiness/gotosocial/testrig"
)
type AuthStandardTestSuite struct {
suite.Suite
- db db.DB
- idp oidc.IDP
- oauthServer oauth.Server
+ db db.DB
+ storage *kv.KVStore
+ mediaManager media.Manager
+ federator federation.Federator
+ processor processing.Processor
+ emailSender email.Sender
+ idp oidc.IDP
+ oauthServer oauth.Server
// standard suite models
testTokens map[string]*gtsmodel.Token
@@ -69,17 +81,26 @@ func (suite *AuthStandardTestSuite) SetupSuite() {
func (suite *AuthStandardTestSuite) SetupTest() {
testrig.InitTestConfig()
- suite.db = testrig.NewTestDB()
testrig.InitTestLog()
+ fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
+ clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1)
+
+ suite.db = testrig.NewTestDB()
+ suite.storage = testrig.NewTestStorage()
+ suite.mediaManager = testrig.NewTestMediaManager(suite.db, suite.storage)
+ suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db, fedWorker), suite.storage, suite.mediaManager, fedWorker)
+ suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil)
+ suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker)
+
suite.oauthServer = testrig.NewTestOauthServer(suite.db)
var err error
suite.idp, err = oidc.NewIDP(context.Background())
if err != nil {
panic(err)
}
- suite.authModule = auth.New(suite.db, suite.oauthServer, suite.idp).(*auth.Module)
- testrig.StandardDBSetup(suite.db, nil)
+ suite.authModule = auth.New(suite.db, suite.oauthServer, suite.idp, suite.processor).(*auth.Module)
+ testrig.StandardDBSetup(suite.db, suite.testAccounts)
}
func (suite *AuthStandardTestSuite) TearDownTest() {
@@ -92,7 +113,7 @@ func (suite *AuthStandardTestSuite) newContext(requestMethod string, requestPath
ctx, engine := gin.CreateTestContext(recorder)
// load templates into the engine
- testrig.ConfigureTemplatesWithGin(engine)
+ testrig.ConfigureTemplatesWithGin(engine, "../../../../web/template")
// create the request
protocol := config.GetProtocol()
diff --git a/internal/api/client/auth/authorize.go b/internal/api/client/auth/authorize.go
index 387b83c1e..6f96484a8 100644
--- a/internal/api/client/auth/authorize.go
+++ b/internal/api/client/auth/authorize.go
@@ -23,9 +23,6 @@ import (
"fmt"
"net/http"
"net/url"
- "strings"
-
- "github.com/sirupsen/logrus"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
@@ -33,18 +30,22 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
+// helpfulAdvice is a handy hint to users;
+// particularly important during the login flow
+var helpfulAdvice = "If you arrived at this error during a login/oauth flow, please try clearing your session cookies and logging in again; if problems persist, make sure you're using the correct credentials"
+
// AuthorizeGETHandler should be served as GET at https://example.org/oauth/authorize
// The idea here is to present an oauth authorize page to the user, with a button
// that they have to click to accept.
func (m *Module) AuthorizeGETHandler(c *gin.Context) {
- l := logrus.WithField("func", "AuthorizeGETHandler")
s := sessions.Default(c)
if _, err := api.NegotiateAccept(c, api.HTMLAcceptHeaders...); err != nil {
- c.HTML(http.StatusNotAcceptable, "error.tmpl", gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
@@ -52,56 +53,75 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) {
// If it's not set, then we don't know yet who the user is, so we need to redirect them to the sign in page.
userID, ok := s.Get(sessionUserID).(string)
if !ok || userID == "" {
- l.Trace("userid was empty, parsing form then redirecting to sign in page")
form := &model.OAuthAuthorize{}
- if err := c.Bind(form); err != nil {
- l.Debugf("invalid auth form: %s", err)
+ if err := c.ShouldBind(form); err != nil {
m.clearSession(s)
- c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet)
return
}
- l.Debugf("parsed auth form: %+v", form)
- if err := extractAuthForm(s, form); err != nil {
- l.Debugf(fmt.Sprintf("error parsing form at /oauth/authorize: %s", err))
+ if errWithCode := saveAuthFormToSession(s, form); errWithCode != nil {
m.clearSession(s)
- c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"error": err.Error()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
+
c.Redirect(http.StatusSeeOther, AuthSignInPath)
return
}
- // We can use the client_id on the session to retrieve info about the app associated with the client_id
+ // use session information to validate app, user, and account for this request
clientID, ok := s.Get(sessionClientID).(string)
if !ok || clientID == "" {
- c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": "no client_id found in session"})
- return
- }
- app := >smodel.Application{}
- if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil {
m.clearSession(s)
- c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
- "error": fmt.Sprintf("no application found for client id %s", clientID),
- })
+ err := fmt.Errorf("key %s was not found in session", sessionClientID)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet)
+ return
+ }
+
+ app := >smodel.Application{}
+ if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil {
+ m.clearSession(s)
+ safe := fmt.Sprintf("application for %s %s could not be retrieved", sessionClientID, clientID)
+ var errWithCode gtserror.WithCode
+ if err == db.ErrNoEntries {
+ errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice)
+ } else {
+ errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice)
+ }
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
- // redirect the user if they have not confirmed their email yet, thier account has not been approved yet,
- // or thier account has been disabled.
user := >smodel.User{}
if err := m.db.GetByID(c.Request.Context(), userID, user); err != nil {
m.clearSession(s)
- c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": err.Error()})
+ safe := fmt.Sprintf("user with id %s could not be retrieved", userID)
+ var errWithCode gtserror.WithCode
+ if err == db.ErrNoEntries {
+ errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice)
+ } else {
+ errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice)
+ }
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
+
acct, err := m.db.GetAccountByID(c.Request.Context(), user.AccountID)
if err != nil {
m.clearSession(s)
- c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": err.Error()})
+ safe := fmt.Sprintf("account with id %s could not be retrieved", user.AccountID)
+ var errWithCode gtserror.WithCode
+ if err == db.ErrNoEntries {
+ errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice)
+ } else {
+ errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice)
+ }
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
- if !ensureUserIsAuthorizedOrRedirect(c, user, acct) {
+
+ if ensureUserIsAuthorizedOrRedirect(c, user, acct) {
return
}
@@ -109,25 +129,27 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) {
redirect, ok := s.Get(sessionRedirectURI).(string)
if !ok || redirect == "" {
m.clearSession(s)
- c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": "no redirect_uri found in session"})
+ err := fmt.Errorf("key %s was not found in session", sessionRedirectURI)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet)
return
}
+
scope, ok := s.Get(sessionScope).(string)
if !ok || scope == "" {
m.clearSession(s)
- c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": "no scope found in session"})
+ err := fmt.Errorf("key %s was not found in session", sessionScope)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet)
return
}
// the authorize template will display a form to the user where they can get some information
// about the app that's trying to authorize, and the scope of the request.
// They can then approve it if it looks OK to them, which will POST to the AuthorizePOSTHandler
- l.Trace("serving authorize html")
c.HTML(http.StatusOK, "authorize.tmpl", gin.H{
"appname": app.Name,
"appwebsite": app.Website,
"redirect": redirect,
- sessionScope: scope,
+ "scope": scope,
"user": acct.Username,
})
}
@@ -136,13 +158,10 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) {
// At this point we assume that the user has A) logged in and B) accepted that the app should act for them,
// so we should proceed with the authentication flow and generate an oauth token for them if we can.
func (m *Module) AuthorizePOSTHandler(c *gin.Context) {
- l := logrus.WithField("func", "AuthorizePOSTHandler")
s := sessions.Default(c)
// We need to retrieve the original form submitted to the authorizeGEThandler, and
// recreate it on the request so that it can be used further by the oauth2 library.
- // So first fetch all the values from the session.
-
errs := []string{}
forceLogin, ok := s.Get(sessionForceLogin).(string)
@@ -152,77 +171,107 @@ func (m *Module) AuthorizePOSTHandler(c *gin.Context) {
responseType, ok := s.Get(sessionResponseType).(string)
if !ok || responseType == "" {
- errs = append(errs, "session missing response_type")
+ errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionResponseType))
}
clientID, ok := s.Get(sessionClientID).(string)
if !ok || clientID == "" {
- errs = append(errs, "session missing client_id")
+ errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionClientID))
}
redirectURI, ok := s.Get(sessionRedirectURI).(string)
if !ok || redirectURI == "" {
- errs = append(errs, "session missing redirect_uri")
+ errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionRedirectURI))
}
scope, ok := s.Get(sessionScope).(string)
if !ok {
- errs = append(errs, "session missing scope")
+ errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionScope))
}
userID, ok := s.Get(sessionUserID).(string)
if !ok {
- errs = append(errs, "session missing userid")
+ errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionUserID))
+ }
+
+ if len(errs) != 0 {
+ errs = append(errs, helpfulAdvice)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(errors.New("one or more missing keys on session during AuthorizePOSTHandler"), errs...), m.processor.InstanceGet)
+ return
}
- // redirect the user if they have not confirmed their email yet, thier account has not been approved yet,
- // or thier account has been disabled.
user := >smodel.User{}
if err := m.db.GetByID(c.Request.Context(), userID, user); err != nil {
m.clearSession(s)
- c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": err.Error()})
+ safe := fmt.Sprintf("user with id %s could not be retrieved", userID)
+ var errWithCode gtserror.WithCode
+ if err == db.ErrNoEntries {
+ errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice)
+ } else {
+ errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice)
+ }
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
+
acct, err := m.db.GetAccountByID(c.Request.Context(), user.AccountID)
if err != nil {
m.clearSession(s)
- c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": err.Error()})
- return
- }
- if !ensureUserIsAuthorizedOrRedirect(c, user, acct) {
+ safe := fmt.Sprintf("account with id %s could not be retrieved", user.AccountID)
+ var errWithCode gtserror.WithCode
+ if err == db.ErrNoEntries {
+ errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice)
+ } else {
+ errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice)
+ }
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
+ if ensureUserIsAuthorizedOrRedirect(c, user, acct) {
+ return
+ }
+
+ // we're done with the session now, so just clear it out
m.clearSession(s)
- if len(errs) != 0 {
- c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"error": strings.Join(errs, ": ")})
- return
+ // we have to set the values on the request form
+ // so that they're picked up by the oauth server
+ c.Request.Form = url.Values{
+ sessionForceLogin: {forceLogin},
+ sessionResponseType: {responseType},
+ sessionClientID: {clientID},
+ sessionRedirectURI: {redirectURI},
+ sessionScope: {scope},
+ sessionUserID: {userID},
}
- // now set the values on the request
- values := url.Values{}
- values.Set(sessionForceLogin, forceLogin)
- values.Set(sessionResponseType, responseType)
- values.Set(sessionClientID, clientID)
- values.Set(sessionRedirectURI, redirectURI)
- values.Set(sessionScope, scope)
- values.Set(sessionUserID, userID)
- c.Request.Form = values
- l.Tracef("values on request set to %+v", c.Request.Form)
-
- // and proceed with authorization using the oauth2 library
if err := m.server.HandleAuthorizeRequest(c.Writer, c.Request); err != nil {
- c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice), m.processor.InstanceGet)
}
}
-// extractAuthForm checks the given OAuthAuthorize form, and stores
-// the values in the form into the session.
-func extractAuthForm(s sessions.Session, form *model.OAuthAuthorize) error {
- // these fields are *required* so check 'em
- if form.ResponseType == "" || form.ClientID == "" || form.RedirectURI == "" {
- return errors.New("missing one of: response_type, client_id or redirect_uri")
+// saveAuthFormToSession checks the given OAuthAuthorize form,
+// and stores the values in the form into the session.
+func saveAuthFormToSession(s sessions.Session, form *model.OAuthAuthorize) gtserror.WithCode {
+ if form == nil {
+ err := errors.New("OAuthAuthorize form was nil")
+ return gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice)
+ }
+
+ if form.ResponseType == "" {
+ err := errors.New("field response_type was not set on OAuthAuthorize form")
+ return gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice)
+ }
+
+ if form.ClientID == "" {
+ err := errors.New("field client_id was not set on OAuthAuthorize form")
+ return gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice)
+ }
+
+ if form.RedirectURI == "" {
+ err := errors.New("field redirect_uri was not set on OAuthAuthorize form")
+ return gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice)
}
// set default scope to read
@@ -237,29 +286,33 @@ func extractAuthForm(s sessions.Session, form *model.OAuthAuthorize) error {
s.Set(sessionRedirectURI, form.RedirectURI)
s.Set(sessionScope, form.Scope)
s.Set(sessionState, uuid.NewString())
- return s.Save()
+
+ if err := s.Save(); err != nil {
+ err := fmt.Errorf("error saving form values onto session: %s", err)
+ return gtserror.NewErrorInternalError(err, helpfulAdvice)
+ }
+
+ return nil
}
-func ensureUserIsAuthorizedOrRedirect(ctx *gin.Context, user *gtsmodel.User, account *gtsmodel.Account) bool {
+func ensureUserIsAuthorizedOrRedirect(ctx *gin.Context, user *gtsmodel.User, account *gtsmodel.Account) (redirected bool) {
if user.ConfirmedAt.IsZero() {
ctx.Redirect(http.StatusSeeOther, CheckYourEmailPath)
- return false
+ redirected = true
+ return
}
if !user.Approved {
ctx.Redirect(http.StatusSeeOther, WaitForApprovalPath)
- return false
+ redirected = true
+ return
}
- if user.Disabled {
+ if user.Disabled || !account.SuspendedAt.IsZero() {
ctx.Redirect(http.StatusSeeOther, AccountDisabledPath)
- return false
+ redirected = true
+ return
}
- if !account.SuspendedAt.IsZero() {
- ctx.Redirect(http.StatusSeeOther, AccountDisabledPath)
- return false
- }
-
- return true
+ return
}
diff --git a/internal/api/client/auth/callback.go b/internal/api/client/auth/callback.go
index a5c58647c..34a4995c8 100644
--- a/internal/api/client/auth/callback.go
+++ b/internal/api/client/auth/callback.go
@@ -30,7 +30,9 @@ import (
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
+ "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/oidc"
"github.com/superseriousbusiness/gotosocial/internal/validate"
@@ -40,11 +42,14 @@ import (
func (m *Module) CallbackGETHandler(c *gin.Context) {
s := sessions.Default(c)
- // first make sure the state set in the cookie is the same as the state returned from the external provider
+ // check the query vs session state parameter to mitigate csrf
+ // https://auth0.com/docs/secure/attack-protection/state-parameters
+
state := c.Query(callbackStateParam)
if state == "" {
m.clearSession(s)
- c.JSON(http.StatusForbidden, gin.H{"error": "state query not found on callback"})
+ err := fmt.Errorf("%s parameter not found on callback query", callbackStateParam)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
@@ -52,84 +57,104 @@ func (m *Module) CallbackGETHandler(c *gin.Context) {
savedState, ok := savedStateI.(string)
if !ok {
m.clearSession(s)
- c.JSON(http.StatusForbidden, gin.H{"error": "state not found in session"})
+ err := fmt.Errorf("key %s was not found in session", sessionState)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
if state != savedState {
m.clearSession(s)
- c.JSON(http.StatusForbidden, gin.H{"error": "state mismatch"})
+ err := errors.New("mismatch between query state and session state")
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
+ // retrieve stored claims using code
code := c.Query(callbackCodeParam)
-
- claims, err := m.idp.HandleCallback(c.Request.Context(), code)
- if err != nil {
+ if code == "" {
m.clearSession(s)
- c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
+ err := fmt.Errorf("%s parameter not found on callback query", callbackCodeParam)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
- // We can use the client_id on the session to retrieve info about the app associated with the client_id
+ claims, errWithCode := m.idp.HandleCallback(c.Request.Context(), code)
+ if errWithCode != nil {
+ m.clearSession(s)
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
+ return
+ }
+
+ // We can use the client_id on the session to retrieve
+ // info about the app associated with the client_id
clientID, ok := s.Get(sessionClientID).(string)
if !ok || clientID == "" {
m.clearSession(s)
- c.JSON(http.StatusInternalServerError, gin.H{"error": "no client_id found in session during callback"})
- return
- }
- app := >smodel.Application{}
- if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil {
- m.clearSession(s)
- c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("no application found for client id %s", clientID)})
+ err := fmt.Errorf("key %s was not found in session", sessionClientID)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet)
return
}
- user, err := m.parseUserFromClaims(c.Request.Context(), claims, net.IP(c.ClientIP()), app.ID)
- if err != nil {
+ app := >smodel.Application{}
+ if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil {
m.clearSession(s)
- c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
+ safe := fmt.Sprintf("application for %s %s could not be retrieved", sessionClientID, clientID)
+ var errWithCode gtserror.WithCode
+ if err == db.ErrNoEntries {
+ errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice)
+ } else {
+ errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice)
+ }
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
+ return
+ }
+
+ user, errWithCode := m.parseUserFromClaims(c.Request.Context(), claims, net.IP(c.ClientIP()), app.ID)
+ if errWithCode != nil {
+ m.clearSession(s)
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
s.Set(sessionUserID, user.ID)
if err := s.Save(); err != nil {
m.clearSession(s)
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
return
}
c.Redirect(http.StatusFound, OauthAuthorizePath)
}
-func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, ip net.IP, appID string) (*gtsmodel.User, error) {
+func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, ip net.IP, appID string) (*gtsmodel.User, gtserror.WithCode) {
if claims.Email == "" {
- return nil, errors.New("no email returned in claims")
+ err := errors.New("no email returned in claims")
+ return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
// see if we already have a user for this email address
+ // if so, we don't need to continue + create one
user := >smodel.User{}
err := m.db.GetWhere(ctx, []db.Where{{Key: "email", Value: claims.Email}}, user)
if err == nil {
- // we do! so we can just return it
return user, nil
}
if err != db.ErrNoEntries {
- // we have an actual error in the database
- return nil, fmt.Errorf("error checking database for email %s: %s", claims.Email, err)
+ err := fmt.Errorf("error checking database for email %s: %s", claims.Email, err)
+ return nil, gtserror.NewErrorInternalError(err)
}
// maybe we have an unconfirmed user
err = m.db.GetWhere(ctx, []db.Where{{Key: "unconfirmed_email", Value: claims.Email}}, user)
if err == nil {
- // user is unconfirmed so return an error
- return nil, fmt.Errorf("user with email address %s is unconfirmed", claims.Email)
+ err := fmt.Errorf("user with email address %s is unconfirmed", claims.Email)
+ return nil, gtserror.NewErrorForbidden(err, err.Error())
}
if err != db.ErrNoEntries {
- // we have an actual error in the database
- return nil, fmt.Errorf("error checking database for email %s: %s", claims.Email, err)
+ err := fmt.Errorf("error checking database for email %s: %s", claims.Email, err)
+ return nil, gtserror.NewErrorInternalError(err)
}
// we don't have a confirmed or unconfirmed user with the claimed email address
@@ -138,10 +163,10 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i
// check if the email address is available for use; if it's not there's nothing we can so
emailAvailable, err := m.db.IsEmailAvailable(ctx, claims.Email)
if err != nil {
- return nil, fmt.Errorf("email %s not available: %s", claims.Email, err)
+ return nil, gtserror.NewErrorBadRequest(err)
}
if !emailAvailable {
- return nil, fmt.Errorf("email %s in use", claims.Email)
+ return nil, gtserror.NewErrorConflict(fmt.Errorf("email address %s is not available", claims.Email))
}
// now we need a username
@@ -149,12 +174,12 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i
// make sure claims.Name is defined since we'll be using that for the username
if claims.Name == "" {
- return nil, errors.New("no name returned in claims")
+ err := errors.New("no name returned in claims")
+ return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
// check if we can just use claims.Name as-is
- err = validate.Username(claims.Name)
- if err == nil {
+ if err = validate.Username(claims.Name); err == nil {
// the name we have on the claims is already a valid username
username = claims.Name
} else {
@@ -166,12 +191,12 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i
// lowercase the whole thing
lower := strings.ToLower(underscored)
// see if this is valid....
- if err := validate.Username(lower); err == nil {
- // we managed to get a valid username
- username = lower
- } else {
- return nil, fmt.Errorf("couldn't parse a valid username from claims.Name value of %s", claims.Name)
+ if err := validate.Username(lower); err != nil {
+ err := fmt.Errorf("couldn't parse a valid username from claims.Name value of %s: %s", claims.Name, err)
+ return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
+ // we managed to get a valid username
+ username = lower
}
var iString string
@@ -185,7 +210,7 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i
for i := 1; !found; i++ {
usernameAvailable, err := m.db.IsUsernameAvailable(ctx, username+iString)
if err != nil {
- return nil, err
+ return nil, gtserror.NewErrorInternalError(err)
}
if usernameAvailable {
// no error so we've found a username that works
@@ -223,7 +248,7 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i
// create the user! this will also create an account and store it in the database so we don't need to do that here
user, err = m.db.NewSignup(ctx, username, "", requireApproval, claims.Email, password, ip, "", appID, emailVerified, admin)
if err != nil {
- return nil, fmt.Errorf("error creating user: %s", err)
+ return nil, gtserror.NewErrorInternalError(err)
}
return user, nil
diff --git a/internal/api/client/auth/signin.go b/internal/api/client/auth/signin.go
index bdf00c663..b8f267f54 100644
--- a/internal/api/client/auth/signin.go
+++ b/internal/api/client/auth/signin.go
@@ -21,14 +21,14 @@ package auth
import (
"context"
"errors"
+ "fmt"
"net/http"
- "github.com/sirupsen/logrus"
-
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"golang.org/x/crypto/bcrypt"
)
@@ -41,64 +41,62 @@ type login struct {
// SignInGETHandler should be served at https://example.org/auth/sign_in.
// The idea is to present a sign in page to the user, where they can enter their username and password.
-// The form will then POST to the sign in page, which will be handled by SignInPOSTHandler
+// The form will then POST to the sign in page, which will be handled by SignInPOSTHandler.
+// If an idp provider is set, then the user will be redirected to that to do their sign in.
func (m *Module) SignInGETHandler(c *gin.Context) {
- l := logrus.WithField("func", "SignInGETHandler")
- l.Trace("entering sign in handler")
-
if _, err := api.NegotiateAccept(c, api.HTMLAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
- if m.idp != nil {
- s := sessions.Default(c)
-
- stateI := s.Get(sessionState)
- state, ok := stateI.(string)
- if !ok {
- m.clearSession(s)
- c.JSON(http.StatusForbidden, gin.H{"error": "state not found in session"})
- return
- }
-
- redirect := m.idp.AuthCodeURL(state)
- l.Debugf("redirecting to external idp at %s", redirect)
- c.Redirect(http.StatusSeeOther, redirect)
+ if m.idp == nil {
+ // no idp provider, use our own funky little sign in page
+ c.HTML(http.StatusOK, "sign-in.tmpl", gin.H{})
return
}
- c.HTML(http.StatusOK, "sign-in.tmpl", gin.H{})
+
+ // idp provider is in use, so redirect to it
+ s := sessions.Default(c)
+
+ stateI := s.Get(sessionState)
+ state, ok := stateI.(string)
+ if !ok {
+ m.clearSession(s)
+ err := fmt.Errorf("key %s was not found in session", sessionState)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
+ return
+ }
+
+ c.Redirect(http.StatusSeeOther, m.idp.AuthCodeURL(state))
}
// SignInPOSTHandler should be served at https://example.org/auth/sign_in.
// The idea is to present a sign in page to the user, where they can enter their username and password.
// The handler will then redirect to the auth handler served at /auth
func (m *Module) SignInPOSTHandler(c *gin.Context) {
- l := logrus.WithField("func", "SignInPOSTHandler")
s := sessions.Default(c)
+
form := &login{}
if err := c.ShouldBind(form); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
m.clearSession(s)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet)
return
}
- l.Tracef("parsed form: %+v", form)
- userid, err := m.ValidatePassword(c.Request.Context(), form.Email, form.Password)
- if err != nil {
- c.String(http.StatusForbidden, err.Error())
- m.clearSession(s)
+ userid, errWithCode := m.ValidatePassword(c.Request.Context(), form.Email, form.Password)
+ if errWithCode != nil {
+ // don't clear session here, so the user can just press back and try again
+ // if they accidentally gave the wrong password or something
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
s.Set(sessionUserID, userid)
if err := s.Save(); err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- m.clearSession(s)
- return
+ err := fmt.Errorf("error saving user id onto session: %s", err)
+ api.ErrorHandler(c, gtserror.NewErrorInternalError(err, helpfulAdvice), m.processor.InstanceGet)
}
- l.Trace("redirecting to auth page")
c.Redirect(http.StatusFound, OauthAuthorizePath)
}
@@ -106,42 +104,34 @@ func (m *Module) SignInPOSTHandler(c *gin.Context) {
// The goal is to authenticate the password against the one for that email
// address stored in the database. If OK, we return the userid (a ulid) for that user,
// so that it can be used in further Oauth flows to generate a token/retreieve an oauth client from the db.
-func (m *Module) ValidatePassword(ctx context.Context, email string, password string) (userid string, err error) {
- l := logrus.WithField("func", "ValidatePassword")
-
- // make sure an email/password was provided and bail if not
+func (m *Module) ValidatePassword(ctx context.Context, email string, password string) (string, gtserror.WithCode) {
if email == "" || password == "" {
- l.Debug("email or password was not provided")
- return incorrectPassword()
+ err := errors.New("email or password was not provided")
+ return incorrectPassword(err)
}
- // first we select the user from the database based on email address, bail if no user found for that email
- gtsUser := >smodel.User{}
-
- if err := m.db.GetWhere(ctx, []db.Where{{Key: "email", Value: email}}, gtsUser); err != nil {
- l.Debugf("user %s was not retrievable from db during oauth authorization attempt: %s", email, err)
- return incorrectPassword()
+ user := >smodel.User{}
+ if err := m.db.GetWhere(ctx, []db.Where{{Key: "email", Value: email}}, user); err != nil {
+ err := fmt.Errorf("user %s was not retrievable from db during oauth authorization attempt: %s", email, err)
+ return incorrectPassword(err)
}
- // make sure a password is actually set and bail if not
- if gtsUser.EncryptedPassword == "" {
- l.Warnf("encrypted password for user %s was empty for some reason", gtsUser.Email)
- return incorrectPassword()
+ if user.EncryptedPassword == "" {
+ err := fmt.Errorf("encrypted password for user %s was empty for some reason", user.Email)
+ return incorrectPassword(err)
}
- // compare the provided password with the encrypted one from the db, bail if they don't match
- if err := bcrypt.CompareHashAndPassword([]byte(gtsUser.EncryptedPassword), []byte(password)); err != nil {
- l.Debugf("password hash didn't match for user %s during login attempt: %s", gtsUser.Email, err)
- return incorrectPassword()
+ if err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte(password)); err != nil {
+ err := fmt.Errorf("password hash didn't match for user %s during login attempt: %s", user.Email, err)
+ return incorrectPassword(err)
}
- // If we've made it this far the email/password is correct, so we can just return the id of the user.
- userid = gtsUser.ID
- l.Tracef("returning (%s, %s)", userid, err)
- return
+ return user.ID, nil
}
-// incorrectPassword is just a little helper function to use in the ValidatePassword function
-func incorrectPassword() (string, error) {
- return "", errors.New("password/email combination was incorrect")
+// incorrectPassword wraps the given error in a gtserror.WithCode, and returns
+// only a generic 'safe' error message to the user, to not give any info away.
+func incorrectPassword(err error) (string, gtserror.WithCode) {
+ safeErr := fmt.Errorf("password/email combination was incorrect")
+ return "", gtserror.NewErrorUnauthorized(err, safeErr.Error(), helpfulAdvice)
}
diff --git a/internal/api/client/auth/token.go b/internal/api/client/auth/token.go
index a0bc7ae93..34fb62940 100644
--- a/internal/api/client/auth/token.go
+++ b/internal/api/client/auth/token.go
@@ -19,11 +19,10 @@
package auth
import (
- "net/http"
"net/url"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/gin-gonic/gin"
)
@@ -40,38 +39,40 @@ type tokenBody struct {
// TokenPOSTHandler should be served as a POST at https://example.org/oauth/token
// The idea here is to serve an oauth access token to a user, which can be used for authorizing against non-public APIs.
func (m *Module) TokenPOSTHandler(c *gin.Context) {
- l := logrus.WithField("func", "TokenPOSTHandler")
- l.Trace("entered TokenPOSTHandler")
-
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
form := &tokenBody{}
- if err := c.ShouldBind(form); err == nil {
- c.Request.Form = url.Values{}
- if form.ClientID != nil {
- c.Request.Form.Set("client_id", *form.ClientID)
- }
- if form.ClientSecret != nil {
- c.Request.Form.Set("client_secret", *form.ClientSecret)
- }
- if form.Code != nil {
- c.Request.Form.Set("code", *form.Code)
- }
- if form.GrantType != nil {
- c.Request.Form.Set("grant_type", *form.GrantType)
- }
- if form.RedirectURI != nil {
- c.Request.Form.Set("redirect_uri", *form.RedirectURI)
- }
- if form.Scope != nil {
- c.Request.Form.Set("scope", *form.Scope)
- }
+ if err := c.ShouldBind(form); err != nil {
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet)
+ return
}
+ c.Request.Form = url.Values{}
+ if form.ClientID != nil {
+ c.Request.Form.Set("client_id", *form.ClientID)
+ }
+ if form.ClientSecret != nil {
+ c.Request.Form.Set("client_secret", *form.ClientSecret)
+ }
+ if form.Code != nil {
+ c.Request.Form.Set("code", *form.Code)
+ }
+ if form.GrantType != nil {
+ c.Request.Form.Set("grant_type", *form.GrantType)
+ }
+ if form.RedirectURI != nil {
+ c.Request.Form.Set("redirect_uri", *form.RedirectURI)
+ }
+ if form.Scope != nil {
+ c.Request.Form.Set("scope", *form.Scope)
+ }
+
+ // pass the writer and request into the oauth server handler, which will
+ // take care of writing the oauth token into the response etc
if err := m.server.HandleTokenRequest(c.Writer, c.Request); err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorInternalError(err, helpfulAdvice), m.processor.InstanceGet)
}
}
diff --git a/internal/api/client/blocks/blocksget.go b/internal/api/client/blocks/blocksget.go
index 75741946b..8d3c51d82 100644
--- a/internal/api/client/blocks/blocksget.go
+++ b/internal/api/client/blocks/blocksget.go
@@ -19,13 +19,13 @@
package blocks
import (
+ "fmt"
"net/http"
"strconv"
- "github.com/sirupsen/logrus"
-
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -80,24 +80,25 @@ import (
// type: array
// items:
// "$ref": "#/definitions/account"
-// '401':
-// description: unauthorized
// '400':
// description: bad request
+// '401':
+// description: unauthorized
// '404':
// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) BlocksGETHandler(c *gin.Context) {
- l := logrus.WithField("func", "PublicTimelineGETHandler")
-
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debugf("error authing: %s", err)
- c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
@@ -118,8 +119,8 @@ func (m *Module) BlocksGETHandler(c *gin.Context) {
if limitString != "" {
i, err := strconv.ParseInt(limitString, 10, 64)
if err != nil {
- l.Debugf("error parsing limit string: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"})
+ err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
limit = int(i)
@@ -127,8 +128,7 @@ func (m *Module) BlocksGETHandler(c *gin.Context) {
resp, errWithCode := m.processor.BlocksGet(c.Request.Context(), authed, maxID, sinceID, limit)
if errWithCode != nil {
- l.Debugf("error from processor BlocksGet: %s", errWithCode)
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/emoji/emojisget.go b/internal/api/client/emoji/emojisget.go
index 0b8c510eb..488aad78b 100644
--- a/internal/api/client/emoji/emojisget.go
+++ b/internal/api/client/emoji/emojisget.go
@@ -5,18 +5,25 @@ import (
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
)
// EmojisGETHandler returns a list of custom emojis enabled on the instance
func (m *Module) EmojisGETHandler(c *gin.Context) {
+ if _, err := oauth.Authed(c, true, true, true, true); err != nil {
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
+ return
+ }
+
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
emojis, errWithCode := m.processor.CustomEmojisGet(c)
if errWithCode != nil {
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/favourites/favouritesget.go b/internal/api/client/favourites/favouritesget.go
index 5a317b2ea..31fd47c90 100644
--- a/internal/api/client/favourites/favouritesget.go
+++ b/internal/api/client/favourites/favouritesget.go
@@ -1,29 +1,26 @@
package favourites
import (
+ "fmt"
"net/http"
"strconv"
- "github.com/sirupsen/logrus"
-
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
// FavouritesGETHandler handles GETting favourites.
func (m *Module) FavouritesGETHandler(c *gin.Context) {
- l := logrus.WithField("func", "PublicTimelineGETHandler")
-
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debugf("error authing: %s", err)
- c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
@@ -44,8 +41,8 @@ func (m *Module) FavouritesGETHandler(c *gin.Context) {
if limitString != "" {
i, err := strconv.ParseInt(limitString, 10, 64)
if err != nil {
- l.Debugf("error parsing limit string: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"})
+ err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
limit = int(i)
@@ -53,8 +50,7 @@ func (m *Module) FavouritesGETHandler(c *gin.Context) {
resp, errWithCode := m.processor.FavedTimelineGet(c.Request.Context(), authed, maxID, minID, limit)
if errWithCode != nil {
- l.Debugf("error from processor FavedTimelineGet: %s", errWithCode)
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/fileserver/servefile.go b/internal/api/client/fileserver/servefile.go
index d503c0fe6..0372de9d8 100644
--- a/internal/api/client/fileserver/servefile.go
+++ b/internal/api/client/fileserver/servefile.go
@@ -19,6 +19,7 @@
package fileserver
import (
+ "fmt"
"io"
"net/http"
@@ -26,6 +27,7 @@ import (
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -34,17 +36,9 @@ import (
// Note: to mitigate scraping attempts, no information should be given out on a bad request except "404 page not found".
// Don't give away account ids or media ids or anything like that; callers shouldn't be able to infer anything.
func (m *FileServer) ServeFile(c *gin.Context) {
- l := logrus.WithFields(logrus.Fields{
- "func": "ServeFile",
- "request_uri": c.Request.RequestURI,
- "user_agent": c.Request.UserAgent(),
- "origin_ip": c.ClientIP(),
- })
- l.Trace("received request")
-
authed, err := oauth.Authed(c, false, false, false, false)
if err != nil {
- c.String(http.StatusNotFound, "404 page not found")
+ api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet)
return
}
@@ -53,29 +47,29 @@ func (m *FileServer) ServeFile(c *gin.Context) {
// "FILE_NAME" consists of two parts, the attachment's database id, a period, and the file extension.
accountID := c.Param(AccountIDKey)
if accountID == "" {
- l.Debug("missing accountID from request")
- c.String(http.StatusNotFound, "404 page not found")
+ err := fmt.Errorf("missing %s from request", AccountIDKey)
+ api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet)
return
}
mediaType := c.Param(MediaTypeKey)
if mediaType == "" {
- l.Debug("missing mediaType from request")
- c.String(http.StatusNotFound, "404 page not found")
+ err := fmt.Errorf("missing %s from request", MediaTypeKey)
+ api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet)
return
}
mediaSize := c.Param(MediaSizeKey)
if mediaSize == "" {
- l.Debug("missing mediaSize from request")
- c.String(http.StatusNotFound, "404 page not found")
+ err := fmt.Errorf("missing %s from request", MediaSizeKey)
+ api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet)
return
}
fileName := c.Param(FileNameKey)
if fileName == "" {
- l.Debug("missing fileName from request")
- c.String(http.StatusNotFound, "404 page not found")
+ err := fmt.Errorf("missing %s from request", FileNameKey)
+ api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet)
return
}
@@ -86,8 +80,7 @@ func (m *FileServer) ServeFile(c *gin.Context) {
FileName: fileName,
})
if errWithCode != nil {
- l.Errorf(errWithCode.Error())
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
@@ -95,7 +88,7 @@ func (m *FileServer) ServeFile(c *gin.Context) {
// if the content is a ReadCloser, close it when we're done
if closer, ok := content.Content.(io.ReadCloser); ok {
if err := closer.Close(); err != nil {
- l.Errorf("error closing readcloser: %s", err)
+ logrus.Errorf("ServeFile: error closing readcloser: %s", err)
}
}
}()
@@ -103,9 +96,9 @@ func (m *FileServer) ServeFile(c *gin.Context) {
// TODO: if the requester only accepts text/html we should try to serve them *something*.
// This is mostly needed because when sharing a link to a gts-hosted file on something like mastodon, the masto servers will
// attempt to look up the content to provide a preview of the link, and they ask for text/html.
- format, err := api.NegotiateAccept(c, api.Offer(content.ContentType))
- if errWithCode != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ format, err := api.NegotiateAccept(c, api.MIME(content.ContentType))
+ if err != nil {
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/filter/filtersget.go b/internal/api/client/filter/filtersget.go
index f0367d40c..8e0a0bb34 100644
--- a/internal/api/client/filter/filtersget.go
+++ b/internal/api/client/filter/filtersget.go
@@ -5,12 +5,19 @@ import (
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
)
// FiltersGETHandler returns a list of filters set by/for the authed account
func (m *Module) FiltersGETHandler(c *gin.Context) {
+ if _, err := oauth.Authed(c, true, true, true, true); err != nil {
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
+ return
+ }
+
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/followrequest/authorize.go b/internal/api/client/followrequest/authorize.go
index 42448846d..528c39f2b 100644
--- a/internal/api/client/followrequest/authorize.go
+++ b/internal/api/client/followrequest/authorize.go
@@ -19,12 +19,12 @@
package followrequest
import (
+ "errors"
"net/http"
- "github.com/sirupsen/logrus"
-
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -62,43 +62,34 @@ import (
// description: bad request
// '401':
// description: unauthorized
-// '403':
-// description: forbidden
// '404':
// description: not found
+// '406':
+// description: not acceptable
// '500':
// description: internal server error
func (m *Module) FollowRequestAuthorizePOSTHandler(c *gin.Context) {
- l := logrus.WithField("func", "FollowRequestAuthorizePOSTHandler")
-
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
- return
- }
-
- if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
originAccountID := c.Param(IDKey)
if originAccountID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no follow request origin account id provided"})
+ err := errors.New("no account id specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
relationship, errWithCode := m.processor.FollowRequestAccept(c.Request.Context(), authed, originAccountID)
if errWithCode != nil {
- l.Debug(errWithCode.Error())
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/followrequest/authorize_test.go b/internal/api/client/followrequest/authorize_test.go
index c5f4c94f2..693380d91 100644
--- a/internal/api/client/followrequest/authorize_test.go
+++ b/internal/api/client/followrequest/authorize_test.go
@@ -82,6 +82,34 @@ func (suite *AuthorizeTestSuite) TestAuthorize() {
suite.Equal(`{"id":"01FHMQX3GAABWSM0S2VZEC2SWC","following":false,"showing_reblogs":false,"notifying":false,"followed_by":true,"blocking":false,"blocked_by":false,"muting":false,"muting_notifications":false,"requested":false,"domain_blocking":false,"endorsed":false,"note":""}`, string(b))
}
+func (suite *AuthorizeTestSuite) TestAuthorizeNoFR() {
+ requestingAccount := suite.testAccounts["remote_account_2"]
+
+ recorder := httptest.NewRecorder()
+ ctx := suite.newContext(recorder, http.MethodPost, []byte{}, fmt.Sprintf("/api/v1/follow_requests/%s/authorize", requestingAccount.ID), "")
+
+ ctx.Params = gin.Params{
+ gin.Param{
+ Key: followrequest.IDKey,
+ Value: requestingAccount.ID,
+ },
+ }
+
+ // call the handler
+ suite.followRequestModule.FollowRequestAuthorizePOSTHandler(ctx)
+
+ suite.Equal(http.StatusNotFound, recorder.Code)
+
+ result := recorder.Result()
+ defer result.Body.Close()
+
+ // check the response
+ b, err := ioutil.ReadAll(result.Body)
+ assert.NoError(suite.T(), err)
+
+ suite.Equal(`{"error":"Not Found"}`, string(b))
+}
+
func TestAuthorizeTestSuite(t *testing.T) {
suite.Run(t, &AuthorizeTestSuite{})
}
diff --git a/internal/api/client/followrequest/get.go b/internal/api/client/followrequest/get.go
index a22278ae4..5dd079ddb 100644
--- a/internal/api/client/followrequest/get.go
+++ b/internal/api/client/followrequest/get.go
@@ -21,10 +21,9 @@ package followrequest
import (
"net/http"
- "github.com/sirupsen/logrus"
-
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -71,34 +70,27 @@ import (
// description: bad request
// '401':
// description: unauthorized
-// '403':
-// description: forbidden
// '404':
// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) FollowRequestGETHandler(c *gin.Context) {
- l := logrus.WithField("func", "FollowRequestGETHandler")
-
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
- return
- }
-
- if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
accts, errWithCode := m.processor.FollowRequestsGet(c.Request.Context(), authed)
if errWithCode != nil {
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/followrequest/reject.go b/internal/api/client/followrequest/reject.go
index 8f4705d9f..c29ccbf5d 100644
--- a/internal/api/client/followrequest/reject.go
+++ b/internal/api/client/followrequest/reject.go
@@ -19,11 +19,12 @@
package followrequest
import (
+ "errors"
"net/http"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -59,43 +60,34 @@ import (
// description: bad request
// '401':
// description: unauthorized
-// '403':
-// description: forbidden
// '404':
// description: not found
+// '406':
+// description: not acceptable
// '500':
// description: internal server error
func (m *Module) FollowRequestRejectPOSTHandler(c *gin.Context) {
- l := logrus.WithField("func", "FollowRequestRejectPOSTHandler")
-
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
- return
- }
-
- if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
originAccountID := c.Param(IDKey)
if originAccountID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no follow request origin account id provided"})
+ err := errors.New("no account id specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
relationship, errWithCode := m.processor.FollowRequestReject(c.Request.Context(), authed, originAccountID)
if errWithCode != nil {
- l.Debug(errWithCode.Error())
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/instance/instanceget.go b/internal/api/client/instance/instanceget.go
index cd6dd32cc..35e842102 100644
--- a/internal/api/client/instance/instanceget.go
+++ b/internal/api/client/instance/instanceget.go
@@ -3,9 +3,9 @@ package instance
import (
"net/http"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/gin-gonic/gin"
)
@@ -30,22 +30,19 @@ import (
// description: "Instance information."
// schema:
// "$ref": "#/definitions/instance"
+// '406':
+// description: not acceptable
// '500':
// description: internal error
func (m *Module) InstanceInformationGETHandler(c *gin.Context) {
- l := logrus.WithField("func", "InstanceInformationGETHandler")
-
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
- host := config.GetHost()
-
- instance, err := m.processor.InstanceGet(c.Request.Context(), host)
- if err != nil {
- l.Debugf("error getting instance from processor: %s", err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
+ instance, errWithCode := m.processor.InstanceGet(c.Request.Context(), config.GetHost())
+ if errWithCode != nil {
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/instance/instancepatch.go b/internal/api/client/instance/instancepatch.go
index e0200cb0f..4e3f1e454 100644
--- a/internal/api/client/instance/instancepatch.go
+++ b/internal/api/client/instance/instancepatch.go
@@ -1,13 +1,13 @@
package instance
import (
+ "errors"
"net/http"
- "github.com/sirupsen/logrus"
-
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -82,52 +82,51 @@ import (
// description: "The newly updated instance."
// schema:
// "$ref": "#/definitions/instance"
-// '401':
-// description: unauthorized
// '400':
// description: bad request
+// '401':
+// description: unauthorized
+// '403':
+// description: forbidden
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) InstanceUpdatePATCHHandler(c *gin.Context) {
- l := logrus.WithField("func", "InstanceUpdatePATCHHandler")
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
- // only admins can update instance settings
if !authed.User.Admin {
- l.Debug("user is not an admin so cannot update instance settings")
- c.JSON(http.StatusUnauthorized, gin.H{"error": "not an admin"})
+ err := errors.New("user is not an admin so cannot update instance settings")
+ api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
return
}
- l.Debug("parsing request form")
form := &model.InstanceSettingsUpdateRequest{}
- if err := c.ShouldBind(&form); err != nil || form == nil {
- l.Debugf("could not parse form from request: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ if err := c.ShouldBind(&form); err != nil {
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
- l.Debugf("parsed form: %+v", form)
-
- // if everything on the form is nil, then nothing has been set and we shouldn't continue
if form.Title == nil && form.ContactUsername == nil && form.ContactEmail == nil && form.ShortDescription == nil && form.Description == nil && form.Terms == nil && form.Avatar == nil && form.Header == nil {
- l.Debugf("could not parse form from request")
- c.JSON(http.StatusBadRequest, gin.H{"error": "empty form submitted"})
+ err := errors.New("empty form submitted")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
i, errWithCode := m.processor.InstancePatch(c.Request.Context(), form)
if errWithCode != nil {
- l.Debugf("error with instance patch request: %s", errWithCode.Error())
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/instance/instancepatch_test.go b/internal/api/client/instance/instancepatch_test.go
index 891a9da21..5ca4f2b7a 100644
--- a/internal/api/client/instance/instancepatch_test.go
+++ b/internal/api/client/instance/instancepatch_test.go
@@ -26,6 +26,7 @@ import (
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/api/client/instance"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -125,6 +126,67 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() {
suite.Equal(`{"uri":"http://localhost:8080","title":"localhost:8080","description":"","short_description":"\u003cp\u003eThis is some html, which is \u003cem\u003eallowed\u003c/em\u003e in short descriptions.\u003c/p\u003e","email":"","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":0,"status_count":16,"user_count":4},"thumbnail":"","max_toot_chars":5000}`, string(b))
}
+func (suite *InstancePatchTestSuite) TestInstancePatch4() {
+ requestBody, w, err := testrig.CreateMultipartFormData(
+ "", "",
+ map[string]string{})
+ if err != nil {
+ panic(err)
+ }
+ bodyBytes := requestBody.Bytes()
+
+ // set up the request
+ recorder := httptest.NewRecorder()
+ ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, instance.InstanceInformationPath, w.FormDataContentType())
+
+ // call the handler
+ suite.instanceModule.InstanceUpdatePATCHHandler(ctx)
+
+ suite.Equal(http.StatusBadRequest, recorder.Code)
+
+ result := recorder.Result()
+ defer result.Body.Close()
+
+ b, err := io.ReadAll(result.Body)
+ suite.NoError(err)
+
+ suite.Equal(`{"error":"Bad Request: empty form submitted"}`, string(b))
+}
+
+func (suite *InstancePatchTestSuite) TestInstancePatch5() {
+ requestBody, w, err := testrig.CreateMultipartFormData(
+ "", "",
+ map[string]string{
+ "short_description": "
This is some html, which is allowed in short descriptions.
",
+ })
+ if err != nil {
+ panic(err)
+ }
+ bodyBytes := requestBody.Bytes()
+
+ // set up the request
+ recorder := httptest.NewRecorder()
+ ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, instance.InstanceInformationPath, w.FormDataContentType())
+
+ ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
+ ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"]))
+ ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
+ ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
+
+ // call the handler
+ suite.instanceModule.InstanceUpdatePATCHHandler(ctx)
+
+ suite.Equal(http.StatusForbidden, recorder.Code)
+
+ result := recorder.Result()
+ defer result.Body.Close()
+
+ b, err := io.ReadAll(result.Body)
+ suite.NoError(err)
+
+ suite.Equal(`{"error":"Forbidden: user is not an admin so cannot update instance settings"}`, string(b))
+}
+
func TestInstancePatchTestSuite(t *testing.T) {
suite.Run(t, &InstancePatchTestSuite{})
}
diff --git a/internal/api/client/list/listsgets.go b/internal/api/client/list/listsgets.go
index 184830290..246a1216a 100644
--- a/internal/api/client/list/listsgets.go
+++ b/internal/api/client/list/listsgets.go
@@ -5,12 +5,19 @@ import (
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
)
// ListsGETHandler returns a list of lists created by/for the authed account
func (m *Module) ListsGETHandler(c *gin.Context) {
+ if _, err := oauth.Authed(c, true, true, true, true); err != nil {
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
+ return
+ }
+
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/media/mediacreate.go b/internal/api/client/media/mediacreate.go
index ed6a12d37..f51d272b6 100644
--- a/internal/api/client/media/mediacreate.go
+++ b/internal/api/client/media/mediacreate.go
@@ -23,12 +23,11 @@ import (
"fmt"
"net/http"
- "github.com/sirupsen/logrus"
-
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -80,46 +79,36 @@ import (
// description: bad request
// '401':
// description: unauthorized
-// '403':
-// description: forbidden
// '422':
// description: unprocessable
+// '500':
+// description: internal server error
func (m *Module) MediaCreatePOSTHandler(c *gin.Context) {
- l := logrus.WithField("func", "statusCreatePOSTHandler")
- authed, err := oauth.Authed(c, true, true, true, true) // posting new media is serious business so we want *everything*
+ authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
- // extract the media create form from the request context
- l.Tracef("parsing request form: %s", c.Request.Form)
form := &model.AttachmentRequest{}
if err := c.ShouldBind(&form); err != nil {
- l.Debugf("error parsing form: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("could not parse form: %s", err)})
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
- // Give the fields on the request form a first pass to make sure the request is superficially valid.
- l.Tracef("validating form %+v", form)
if err := validateCreateMedia(form); err != nil {
- l.Debugf("error validating form: %s", err)
- c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
- l.Debug("calling processor media create func")
- apiAttachment, err := m.processor.MediaCreate(c.Request.Context(), authed, form)
+ apiAttachment, errWithCode := m.processor.MediaCreate(c.Request.Context(), authed, form)
if err != nil {
- l.Debugf("error creating attachment: %s", err)
- c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
@@ -143,6 +132,7 @@ func validateCreateMedia(form *model.AttachmentRequest) error {
if maxImageSize > maxSize {
maxSize = maxImageSize
}
+
if form.File.Size > int64(maxSize) {
return fmt.Errorf("file size limit exceeded: limit is %d bytes but attachment was %d bytes", maxSize, form.File.Size)
}
diff --git a/internal/api/client/media/mediacreate_test.go b/internal/api/client/media/mediacreate_test.go
index f39e756f7..47dd9ac3f 100644
--- a/internal/api/client/media/mediacreate_test.go
+++ b/internal/api/client/media/mediacreate_test.go
@@ -247,15 +247,14 @@ func (suite *MediaCreateTestSuite) TestMediaCreateLongDescription() {
suite.mediaModule.MediaCreatePOSTHandler(ctx)
// check response
- suite.EqualValues(http.StatusUnprocessableEntity, recorder.Code)
+ suite.EqualValues(http.StatusBadRequest, recorder.Code)
result := recorder.Result()
defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body)
suite.NoError(err)
- expectedErr := fmt.Sprintf(`{"error":"image description length must be between 0 and 500 characters (inclusive), but provided image description was %d chars"}`, len(description))
- suite.Equal(expectedErr, string(b))
+ suite.Equal(`{"error":"Bad Request: image description length must be between 0 and 500 characters (inclusive), but provided image description was 6667 chars"}`, string(b))
}
func (suite *MediaCreateTestSuite) TestMediaCreateTooShortDescription() {
diff --git a/internal/api/client/media/mediaget.go b/internal/api/client/media/mediaget.go
index 46057d219..c2a0ab7a0 100644
--- a/internal/api/client/media/mediaget.go
+++ b/internal/api/client/media/mediaget.go
@@ -19,12 +19,12 @@
package media
import (
+ "errors"
"net/http"
- "github.com/sirupsen/logrus"
-
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -59,33 +59,34 @@ import (
// description: bad request
// '401':
// description: unauthorized
-// '403':
-// description: forbidden
-// '422':
-// description: unprocessable
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) MediaGETHandler(c *gin.Context) {
- l := logrus.WithField("func", "MediaGETHandler")
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
attachmentID := c.Param(IDKey)
if attachmentID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no attachment ID given in request"})
+ err := errors.New("no attachment id specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
attachment, errWithCode := m.processor.MediaGet(c.Request.Context(), authed, attachmentID)
if errWithCode != nil {
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/media/mediaupdate.go b/internal/api/client/media/mediaupdate.go
index 21b8c9c8e..32aef9cff 100644
--- a/internal/api/client/media/mediaupdate.go
+++ b/internal/api/client/media/mediaupdate.go
@@ -23,12 +23,11 @@ import (
"fmt"
"net/http"
- "github.com/sirupsen/logrus"
-
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -89,50 +88,45 @@ import (
// description: bad request
// '401':
// description: unauthorized
-// '403':
-// description: forbidden
-// '422':
-// description: unprocessable
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) MediaPUTHandler(c *gin.Context) {
- l := logrus.WithField("func", "MediaGETHandler")
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
attachmentID := c.Param(IDKey)
if attachmentID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no attachment ID given in request"})
+ err := errors.New("no attachment id specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
- // extract the media update form from the request context
- l.Tracef("parsing request form: %s", c.Request.Form)
- var form model.AttachmentUpdateRequest
- if err := c.ShouldBind(&form); err != nil {
- l.Debugf("could not parse form from request: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"})
+ form := &model.AttachmentUpdateRequest{}
+ if err := c.ShouldBind(form); err != nil {
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
- // Give the fields on the request form a first pass to make sure the request is superficially valid.
- l.Tracef("validating form %+v", form)
- if err := validateUpdateMedia(&form); err != nil {
- l.Debugf("error validating form: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ if err := validateUpdateMedia(form); err != nil {
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
- attachment, errWithCode := m.processor.MediaUpdate(c.Request.Context(), authed, attachmentID, &form)
+ attachment, errWithCode := m.processor.MediaUpdate(c.Request.Context(), authed, attachmentID, form)
if errWithCode != nil {
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/media/mediaupdate_test.go b/internal/api/client/media/mediaupdate_test.go
index fbf794e1d..d25eddd4d 100644
--- a/internal/api/client/media/mediaupdate_test.go
+++ b/internal/api/client/media/mediaupdate_test.go
@@ -232,7 +232,7 @@ func (suite *MediaUpdateTestSuite) TestUpdateImageShortDescription() {
suite.NoError(err)
// reply should be an error message
- suite.Equal(`{"error":"image description length must be between 50 and 500 characters (inclusive), but provided image description was 16 chars"}`, string(b))
+ suite.Equal(`{"error":"Bad Request: image description length must be between 50 and 500 characters (inclusive), but provided image description was 16 chars"}`, string(b))
}
func TestMediaUpdateTestSuite(t *testing.T) {
diff --git a/internal/api/client/notification/notificationsget.go b/internal/api/client/notification/notificationsget.go
index b6f7cdd01..88220107e 100644
--- a/internal/api/client/notification/notificationsget.go
+++ b/internal/api/client/notification/notificationsget.go
@@ -19,34 +19,26 @@
package notification
import (
+ "fmt"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
// NotificationsGETHandler serves a list of notifications to the caller, with the desired query parameters
func (m *Module) NotificationsGETHandler(c *gin.Context) {
- l := logrus.WithFields(logrus.Fields{
- "func": "NotificationsGETHandler",
- "request_uri": c.Request.RequestURI,
- "user_agent": c.Request.UserAgent(),
- "origin_ip": c.ClientIP(),
- })
- l.Debugf("entering function")
-
- authed, err := oauth.Authed(c, true, true, true, true) // we don't really need an app here but we want everything else
+ authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Errorf("error authing status faved by request: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
@@ -55,8 +47,8 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) {
if limitString != "" {
i, err := strconv.ParseInt(limitString, 10, 64)
if err != nil {
- l.Debugf("error parsing limit string: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"})
+ err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
limit = int(i)
@@ -76,8 +68,7 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) {
resp, errWithCode := m.processor.NotificationsGet(c.Request.Context(), authed, limit, maxID, sinceID)
if errWithCode != nil {
- l.Debugf("error processing notifications get: %s", errWithCode.Error())
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/search/searchget.go b/internal/api/client/search/searchget.go
index f9cbba38a..8db49ebfd 100644
--- a/internal/api/client/search/searchget.go
+++ b/internal/api/client/search/searchget.go
@@ -19,14 +19,15 @@
package search
import (
+ "errors"
"fmt"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -52,50 +53,44 @@ import (
// type: array
// items:
// "$ref": "#/definitions/searchResult"
-// '401':
-// description: unauthorized
// '400':
// description: bad request
+// '401':
+// description: unauthorized
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) SearchGETHandler(c *gin.Context) {
- l := logrus.WithFields(logrus.Fields{
- "func": "SearchGETHandler",
- "request_uri": c.Request.RequestURI,
- "user_agent": c.Request.UserAgent(),
- "origin_ip": c.ClientIP(),
- })
- l.Debugf("entering function")
-
- authed, err := oauth.Authed(c, true, true, true, true) // we don't really need an app here but we want everything else
+ authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Errorf("error authing search request: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
- accountID := c.Query(AccountIDKey)
- maxID := c.Query(MaxIDKey)
- minID := c.Query(MinIDKey)
- searchType := c.Query(TypeKey)
-
excludeUnreviewed := false
excludeUnreviewedString := c.Query(ExcludeUnreviewedKey)
if excludeUnreviewedString != "" {
var err error
excludeUnreviewed, err = strconv.ParseBool(excludeUnreviewedString)
if err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("couldn't parse param %s: %s", excludeUnreviewedString, err)})
+ err := fmt.Errorf("error parsing %s: %s", ExcludeUnreviewedKey, err)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
}
query := c.Query(QueryKey)
if query == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "query parameter q was empty"})
+ err := errors.New("query parameter q was empty")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
@@ -105,18 +100,19 @@ func (m *Module) SearchGETHandler(c *gin.Context) {
var err error
resolve, err = strconv.ParseBool(resolveString)
if err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("couldn't parse param %s: %s", resolveString, err)})
+ err := fmt.Errorf("error parsing %s: %s", ResolveKey, err)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
}
- limit := 20
+ limit := 2
limitString := c.Query(LimitKey)
if limitString != "" {
i, err := strconv.ParseInt(limitString, 10, 64)
if err != nil {
- l.Debugf("error parsing limit string: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"})
+ err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
limit = int(i)
@@ -133,18 +129,12 @@ func (m *Module) SearchGETHandler(c *gin.Context) {
if offsetString != "" {
i, err := strconv.ParseInt(offsetString, 10, 64)
if err != nil {
- l.Debugf("error parsing offset string: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse offset query param"})
+ err := fmt.Errorf("error parsing %s: %s", OffsetKey, err)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
offset = int(i)
}
- if limit > 40 {
- limit = 40
- }
- if limit < 1 {
- limit = 1
- }
following := false
followingString := c.Query(FollowingKey)
@@ -152,16 +142,17 @@ func (m *Module) SearchGETHandler(c *gin.Context) {
var err error
following, err = strconv.ParseBool(followingString)
if err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("couldn't parse param %s: %s", followingString, err)})
+ err := fmt.Errorf("error parsing %s: %s", FollowingKey, err)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
}
searchQuery := &model.SearchQuery{
- AccountID: accountID,
- MaxID: maxID,
- MinID: minID,
- Type: searchType,
+ AccountID: c.Query(AccountIDKey),
+ MaxID: c.Query(MaxIDKey),
+ MinID: c.Query(MinIDKey),
+ Type: c.Query(TypeKey),
ExcludeUnreviewed: excludeUnreviewed,
Query: query,
Resolve: resolve,
@@ -172,8 +163,7 @@ func (m *Module) SearchGETHandler(c *gin.Context) {
results, errWithCode := m.processor.SearchGet(c.Request.Context(), authed, searchQuery)
if errWithCode != nil {
- l.Debugf("error searching: %s", errWithCode.Error())
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/status/statusboost.go b/internal/api/client/status/statusboost.go
index 25b2cf0e7..0642ad92e 100644
--- a/internal/api/client/status/statusboost.go
+++ b/internal/api/client/status/statusboost.go
@@ -19,11 +19,12 @@
package status
import (
+ "errors"
"net/http"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -66,37 +67,32 @@ import (
// description: forbidden
// '404':
// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) StatusBoostPOSTHandler(c *gin.Context) {
- l := logrus.WithFields(logrus.Fields{
- "func": "StatusBoostPOSTHandler",
- "request_uri": c.Request.RequestURI,
- "user_agent": c.Request.UserAgent(),
- "origin_ip": c.ClientIP(),
- })
- l.Debugf("entering function")
-
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debug("not authed so can't boost status")
- c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
targetStatusID := c.Param(IDKey)
if targetStatusID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
+ err := errors.New("no status id specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
apiStatus, errWithCode := m.processor.StatusBoost(c.Request.Context(), authed, targetStatusID)
if errWithCode != nil {
- l.Debugf("error processing status boost: %s", errWithCode.Error())
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/status/statusboost_test.go b/internal/api/client/status/statusboost_test.go
index 42693d9e0..e33b582e8 100644
--- a/internal/api/client/status/statusboost_test.go
+++ b/internal/api/client/status/statusboost_test.go
@@ -134,13 +134,13 @@ func (suite *StatusBoostTestSuite) TestPostUnboostable() {
suite.statusModule.StatusBoostPOSTHandler(ctx)
// check response
- suite.EqualValues(http.StatusForbidden, recorder.Code) // we 403 unboostable statuses
+ suite.Equal(http.StatusForbidden, recorder.Code) // we 403 unboostable statuses
result := recorder.Result()
defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body)
assert.NoError(suite.T(), err)
- assert.Equal(suite.T(), `{"error":"forbidden"}`, string(b))
+ assert.Equal(suite.T(), `{"error":"Forbidden"}`, string(b))
}
// try to boost a status that's not visible to the user
@@ -177,13 +177,7 @@ func (suite *StatusBoostTestSuite) TestPostNotVisible() {
suite.statusModule.StatusBoostPOSTHandler(ctx)
// check response
- suite.EqualValues(http.StatusNotFound, recorder.Code) // we 404 statuses that aren't visible
-
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- assert.NoError(suite.T(), err)
- assert.Equal(suite.T(), `{"error":"404 not found"}`, string(b))
+ suite.Equal(http.StatusNotFound, recorder.Code) // we 404 statuses that aren't visible
}
func TestStatusBoostTestSuite(t *testing.T) {
diff --git a/internal/api/client/status/statusboostedby.go b/internal/api/client/status/statusboostedby.go
index bd2578a12..382b38f7b 100644
--- a/internal/api/client/status/statusboostedby.go
+++ b/internal/api/client/status/statusboostedby.go
@@ -23,6 +23,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
+ "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -84,10 +85,9 @@ func (m *Module) StatusBoostedByGETHandler(c *gin.Context) {
return
}
- apiAccounts, err := m.processor.StatusBoostedBy(c.Request.Context(), authed, targetStatusID)
- if err != nil {
- l.Debugf("error processing status boosted by request: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
+ apiAccounts, errWithCode := m.processor.StatusBoostedBy(c.Request.Context(), authed, targetStatusID)
+ if errWithCode != nil {
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/status/statuscontext.go b/internal/api/client/status/statuscontext.go
index 32a0a89ba..0daefaec1 100644
--- a/internal/api/client/status/statuscontext.go
+++ b/internal/api/client/status/statuscontext.go
@@ -19,11 +19,12 @@
package status
import (
+ "errors"
"net/http"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -65,37 +66,32 @@ import (
// description: forbidden
// '404':
// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) StatusContextGETHandler(c *gin.Context) {
- l := logrus.WithFields(logrus.Fields{
- "func": "StatusContextGETHandler",
- "request_uri": c.Request.RequestURI,
- "user_agent": c.Request.UserAgent(),
- "origin_ip": c.ClientIP(),
- })
- l.Debugf("entering function")
-
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Errorf("error authing status context request: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
targetStatusID := c.Param(IDKey)
if targetStatusID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
+ err := errors.New("no status id specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
statusContext, errWithCode := m.processor.StatusGetContext(c.Request.Context(), authed, targetStatusID)
if errWithCode != nil {
- l.Debugf("error getting status context: %s", errWithCode.Error())
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/status/statuscreate.go b/internal/api/client/status/statuscreate.go
index 2813efb90..dce6f1296 100644
--- a/internal/api/client/status/statuscreate.go
+++ b/internal/api/client/status/statuscreate.go
@@ -23,12 +23,11 @@ import (
"fmt"
"net/http"
- "github.com/sirupsen/logrus"
-
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/validate"
)
@@ -61,58 +60,44 @@ import (
// description: "The newly created status."
// schema:
// "$ref": "#/definitions/status"
-// '401':
-// description: unauthorized
// '400':
// description: bad request
+// '401':
+// description: unauthorized
+// '403':
+// description: forbidden
// '404':
// description: not found
+// '406':
+// description: not acceptable
// '500':
-// description: internal error
+// description: internal server error
func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {
- l := logrus.WithField("func", "statusCreatePOSTHandler")
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
- // First check this user/account is permitted to post new statuses.
- // There's no point continuing otherwise.
- if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"})
- return
- }
-
- // extract the status create form from the request context
- l.Debugf("parsing request form: %s", c.Request.Form)
form := &model.AdvancedStatusCreateForm{}
- if err := c.ShouldBind(form); err != nil || form == nil {
- l.Debugf("could not parse form from request: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"})
+ if err := c.ShouldBind(form); err != nil {
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
- l.Debugf("handling status request form: %+v", form)
- // Give the fields on the request form a first pass to make sure the request is superficially valid.
- l.Tracef("validating form %+v", form)
if err := validateCreateStatus(form); err != nil {
- l.Debugf("error validating form: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
- apiStatus, err := m.processor.StatusCreate(c.Request.Context(), authed, form)
- if err != nil {
- l.Debugf("error processing status create: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
+ apiStatus, errWithCode := m.processor.StatusCreate(c.Request.Context(), authed, form)
+ if errWithCode != nil {
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
@@ -120,7 +105,6 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {
}
func validateCreateStatus(form *model.AdvancedStatusCreateForm) error {
- // validate that, structurally, we have a valid status/post
if form.Status == "" && form.MediaIDs == nil && form.Poll == nil {
return errors.New("no status, media, or poll provided")
}
@@ -135,19 +119,16 @@ func validateCreateStatus(form *model.AdvancedStatusCreateForm) error {
maxPollChars := config.GetStatusesPollOptionMaxChars()
maxCwChars := config.GetStatusesCWMaxChars()
- // validate status
if form.Status != "" {
if len(form.Status) > maxChars {
return fmt.Errorf("status too long, %d characters provided but limit is %d", len(form.Status), maxChars)
}
}
- // validate media attachments
if len(form.MediaIDs) > maxMediaFiles {
return fmt.Errorf("too many media files attached to status, %d attached but limit is %d", len(form.MediaIDs), maxMediaFiles)
}
- // validate poll
if form.Poll != nil {
if form.Poll.Options == nil {
return errors.New("poll with no options")
@@ -162,14 +143,12 @@ func validateCreateStatus(form *model.AdvancedStatusCreateForm) error {
}
}
- // validate spoiler text/cw
if form.SpoilerText != "" {
if len(form.SpoilerText) > maxCwChars {
return fmt.Errorf("content-warning/spoilertext too long, %d characters provided but limit is %d", len(form.SpoilerText), maxCwChars)
}
}
- // validate post language
if form.Language != "" {
if err := validate.Language(form.Language); err != nil {
return err
diff --git a/internal/api/client/status/statuscreate_test.go b/internal/api/client/status/statuscreate_test.go
index a1df25547..61d0a5ee4 100644
--- a/internal/api/client/status/statuscreate_test.go
+++ b/internal/api/client/status/statuscreate_test.go
@@ -256,7 +256,7 @@ func (suite *StatusCreateTestSuite) TestReplyToNonexistentStatus() {
defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body)
suite.NoError(err)
- suite.Equal(`{"error":"bad request"}`, string(b))
+ suite.Equal(`{"error":"Bad Request: status with id 3759e7ef-8ee1-4c0c-86f6-8b70b9ad3d50 not replyable because it doesn't exist"}`, string(b))
}
// Post a reply to the status of a local user that allows replies.
diff --git a/internal/api/client/status/statusdelete.go b/internal/api/client/status/statusdelete.go
index 2b6a235f7..f9adccb57 100644
--- a/internal/api/client/status/statusdelete.go
+++ b/internal/api/client/status/statusdelete.go
@@ -19,11 +19,12 @@
package status
import (
+ "errors"
"net/http"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -65,43 +66,32 @@ import (
// description: forbidden
// '404':
// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) StatusDELETEHandler(c *gin.Context) {
- l := logrus.WithFields(logrus.Fields{
- "func": "StatusDELETEHandler",
- "request_uri": c.Request.RequestURI,
- "user_agent": c.Request.UserAgent(),
- "origin_ip": c.ClientIP(),
- })
- l.Debugf("entering function")
-
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debug("not authed so can't delete status")
- c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
targetStatusID := c.Param(IDKey)
if targetStatusID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
+ err := errors.New("no status id specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
- apiStatus, err := m.processor.StatusDelete(c.Request.Context(), authed, targetStatusID)
- if err != nil {
- l.Debugf("error processing status delete: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
- return
- }
-
- // the status was already gone/never existed
- if apiStatus == nil {
- c.JSON(http.StatusNotFound, gin.H{"error": "Record not found"})
+ apiStatus, errWithCode := m.processor.StatusDelete(c.Request.Context(), authed, targetStatusID)
+ if errWithCode != nil {
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/status/statusfave.go b/internal/api/client/status/statusfave.go
index 81f70da83..29f74316a 100644
--- a/internal/api/client/status/statusfave.go
+++ b/internal/api/client/status/statusfave.go
@@ -19,11 +19,12 @@
package status
import (
+ "errors"
"net/http"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -62,37 +63,32 @@ import (
// description: forbidden
// '404':
// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) StatusFavePOSTHandler(c *gin.Context) {
- l := logrus.WithFields(logrus.Fields{
- "func": "StatusFavePOSTHandler",
- "request_uri": c.Request.RequestURI,
- "user_agent": c.Request.UserAgent(),
- "origin_ip": c.ClientIP(),
- })
- l.Debugf("entering function")
-
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debug("not authed so can't fave status")
- c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
targetStatusID := c.Param(IDKey)
if targetStatusID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
+ err := errors.New("no status id specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
- apiStatus, err := m.processor.StatusFave(c.Request.Context(), authed, targetStatusID)
- if err != nil {
- l.Debugf("error processing status fave: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
+ apiStatus, errWithCode := m.processor.StatusFave(c.Request.Context(), authed, targetStatusID)
+ if errWithCode != nil {
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/status/statusfave_test.go b/internal/api/client/status/statusfave_test.go
index 144296ebd..5b10ba9b3 100644
--- a/internal/api/client/status/statusfave_test.go
+++ b/internal/api/client/status/statusfave_test.go
@@ -118,13 +118,13 @@ func (suite *StatusFaveTestSuite) TestPostUnfaveable() {
suite.statusModule.StatusFavePOSTHandler(ctx)
// check response
- suite.EqualValues(http.StatusBadRequest, recorder.Code)
+ suite.EqualValues(http.StatusForbidden, recorder.Code)
result := recorder.Result()
defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body)
assert.NoError(suite.T(), err)
- assert.Equal(suite.T(), `{"error":"bad request"}`, string(b))
+ assert.Equal(suite.T(), `{"error":"Forbidden"}`, string(b))
}
func TestStatusFaveTestSuite(t *testing.T) {
diff --git a/internal/api/client/status/statusfavedby.go b/internal/api/client/status/statusfavedby.go
index 3e3a18136..579df6432 100644
--- a/internal/api/client/status/statusfavedby.go
+++ b/internal/api/client/status/statusfavedby.go
@@ -19,11 +19,12 @@
package status
import (
+ "errors"
"net/http"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -63,37 +64,32 @@ import (
// description: forbidden
// '404':
// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) StatusFavedByGETHandler(c *gin.Context) {
- l := logrus.WithFields(logrus.Fields{
- "func": "statusGETHandler",
- "request_uri": c.Request.RequestURI,
- "user_agent": c.Request.UserAgent(),
- "origin_ip": c.ClientIP(),
- })
- l.Debugf("entering function")
-
- authed, err := oauth.Authed(c, true, true, true, true) // we don't really need an app here but we want everything else
+ authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Errorf("error authing status faved by request: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
targetStatusID := c.Param(IDKey)
if targetStatusID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
+ err := errors.New("no status id specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
- apiAccounts, err := m.processor.StatusFavedBy(c.Request.Context(), authed, targetStatusID)
- if err != nil {
- l.Debugf("error processing status faved by request: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
+ apiAccounts, errWithCode := m.processor.StatusFavedBy(c.Request.Context(), authed, targetStatusID)
+ if errWithCode != nil {
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/status/statusget.go b/internal/api/client/status/statusget.go
index 0730bcdda..9a8b406dc 100644
--- a/internal/api/client/status/statusget.go
+++ b/internal/api/client/status/statusget.go
@@ -19,11 +19,12 @@
package status
import (
+ "errors"
"net/http"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -54,45 +55,40 @@ import (
// description: "The requested created status."
// schema:
// "$ref": "#/definitions/status"
-// '401':
-// description: unauthorized
// '400':
// description: bad request
+// '401':
+// description: unauthorized
+// '403':
+// description: forbidden
// '404':
// description: not found
+// '406':
+// description: not acceptable
// '500':
-// description: internal error
+// description: internal server error
func (m *Module) StatusGETHandler(c *gin.Context) {
- l := logrus.WithFields(logrus.Fields{
- "func": "statusGETHandler",
- "request_uri": c.Request.RequestURI,
- "user_agent": c.Request.UserAgent(),
- "origin_ip": c.ClientIP(),
- })
- l.Debugf("entering function")
-
- authed, err := oauth.Authed(c, false, false, false, false)
+ authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Errorf("error authing status faved by request: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
targetStatusID := c.Param(IDKey)
if targetStatusID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
+ err := errors.New("no status id specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
- apiStatus, err := m.processor.StatusGet(c.Request.Context(), authed, targetStatusID)
- if err != nil {
- l.Debugf("error processing status get: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
+ apiStatus, errWithCode := m.processor.StatusGet(c.Request.Context(), authed, targetStatusID)
+ if errWithCode != nil {
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/status/statusunboost.go b/internal/api/client/status/statusunboost.go
index 7d6052500..b3e921bb7 100644
--- a/internal/api/client/status/statusunboost.go
+++ b/internal/api/client/status/statusunboost.go
@@ -19,11 +19,12 @@
package status
import (
+ "errors"
"net/http"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -63,37 +64,32 @@ import (
// description: forbidden
// '404':
// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) StatusUnboostPOSTHandler(c *gin.Context) {
- l := logrus.WithFields(logrus.Fields{
- "func": "StatusUnboostPOSTHandler",
- "request_uri": c.Request.RequestURI,
- "user_agent": c.Request.UserAgent(),
- "origin_ip": c.ClientIP(),
- })
- l.Debugf("entering function")
-
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debug("not authed so can't unboost status")
- c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
targetStatusID := c.Param(IDKey)
if targetStatusID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
+ err := errors.New("no status id specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
apiStatus, errWithCode := m.processor.StatusUnboost(c.Request.Context(), authed, targetStatusID)
if errWithCode != nil {
- l.Debugf("error processing status unboost: %s", errWithCode.Error())
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/status/statusunfave.go b/internal/api/client/status/statusunfave.go
index 5c5c7e701..19f217160 100644
--- a/internal/api/client/status/statusunfave.go
+++ b/internal/api/client/status/statusunfave.go
@@ -19,11 +19,12 @@
package status
import (
+ "errors"
"net/http"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -62,37 +63,32 @@ import (
// description: forbidden
// '404':
// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
func (m *Module) StatusUnfavePOSTHandler(c *gin.Context) {
- l := logrus.WithFields(logrus.Fields{
- "func": "StatusUnfavePOSTHandler",
- "request_uri": c.Request.RequestURI,
- "user_agent": c.Request.UserAgent(),
- "origin_ip": c.ClientIP(),
- })
- l.Debugf("entering function")
-
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debug("not authed so can't unfave status")
- c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
targetStatusID := c.Param(IDKey)
if targetStatusID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
+ err := errors.New("no status id specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
- apiStatus, err := m.processor.StatusUnfave(c.Request.Context(), authed, targetStatusID)
- if err != nil {
- l.Debugf("error processing status unfave: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
+ apiStatus, errWithCode := m.processor.StatusUnfave(c.Request.Context(), authed, targetStatusID)
+ if errWithCode != nil {
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/streaming/stream.go b/internal/api/client/streaming/stream.go
index c01bf2216..2c18e4e41 100644
--- a/internal/api/client/streaming/stream.go
+++ b/internal/api/client/streaming/stream.go
@@ -2,14 +2,24 @@ package streaming
import (
"fmt"
- "github.com/sirupsen/logrus"
"net/http"
"time"
+ "github.com/sirupsen/logrus"
+ "github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
+var wsUpgrader = websocket.Upgrader{
+ ReadBufferSize: 1024,
+ WriteBufferSize: 1024,
+ // we expect cors requests (via eg., pinafore.social) so be lenient
+ CheckOrigin: func(r *http.Request) bool { return true },
+}
+
// StreamGETHandler swagger:operation GET /api/v1/streaming streamGet
//
// Initiate a websocket connection for live streaming of statuses and notifications.
@@ -108,79 +118,78 @@ import (
// '400':
// description: bad request
func (m *Module) StreamGETHandler(c *gin.Context) {
- l := logrus.WithField("func", "StreamGETHandler")
-
streamType := c.Query(StreamQueryKey)
if streamType == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("no stream type provided under query key %s", StreamQueryKey)})
+ err := fmt.Errorf("no stream type provided under query key %s", StreamQueryKey)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
accessToken := c.Query(AccessTokenQueryKey)
if accessToken == "" {
- c.JSON(http.StatusUnauthorized, gin.H{"error": fmt.Sprintf("no access token provided under query key %s", AccessTokenQueryKey)})
+ err := fmt.Errorf("no access token provided under query key %s", AccessTokenQueryKey)
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
- // make sure a valid token has been provided and obtain the associated account
- account, err := m.processor.AuthorizeStreamingRequest(c.Request.Context(), accessToken)
- if err != nil {
- c.JSON(http.StatusUnauthorized, gin.H{"error": "could not authorize with given token"})
- return
- }
-
- // prepare to upgrade the connection to a websocket connection
- upgrader := websocket.Upgrader{
- ReadBufferSize: 1024,
- WriteBufferSize: 1024,
- CheckOrigin: func(r *http.Request) bool {
- // we fully expect cors requests (via something like pinafore.social) so we should be lenient here
- return true
- },
- }
-
- // do the actual upgrade here
- conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
- if err != nil {
- l.Infof("error upgrading websocket connection: %s", err)
- return
- }
- defer conn.Close() // whatever happens, when we leave this function we want to close the websocket connection
-
- // inform the processor that we have a new connection and want a s for it
- s, errWithCode := m.processor.OpenStreamForAccount(c.Request.Context(), account, streamType)
+ account, errWithCode := m.processor.AuthorizeStreamingRequest(c.Request.Context(), accessToken)
if errWithCode != nil {
- c.JSON(errWithCode.Code(), errWithCode.Safe())
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
- defer close(s.Hangup) // closing stream.Hangup indicates that we've finished with the connection (the client has gone), so we want to do this on exiting this handler
- // spawn a new ticker for pinging the connection periodically
- t := time.NewTicker(30 * time.Second)
+ stream, errWithCode := m.processor.OpenStreamForAccount(c.Request.Context(), account, streamType)
+ if errWithCode != nil {
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
+ return
+ }
- // we want to stay in the sendloop as long as possible while the client is connected -- the only thing that should break the loop is if the client leaves or something else goes wrong
-sendLoop:
+ l := logrus.WithFields(logrus.Fields{
+ "account": account.Username,
+ "path": BasePath,
+ "streamID": stream.ID,
+ "streamType": streamType,
+ })
+
+ wsConn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil)
+ if err != nil {
+ // If the upgrade fails, then Upgrade replies to the client with an HTTP error response.
+ // Because websocket issues are a pretty common source of headaches, we should also log
+ // this at Error to make this plenty visible and help admins out a bit.
+ l.Errorf("error upgrading websocket connection: %s", err)
+ close(stream.Hangup)
+ return
+ }
+
+ defer func() {
+ // cleanup
+ wsConn.Close()
+ close(stream.Hangup)
+ }()
+
+ streamTicker := time.NewTicker(30 * time.Second)
+
+ // We want to stay in the loop as long as possible while the client is connected.
+ // The only thing that should break the loop is if the client leaves or the connection becomes unhealthy.
+ //
+ // If the loop does break, we expect the client to reattempt connection, so it's cheap to leave + try again
+wsLoop:
for {
select {
- case m := <-s.Messages:
- // we've got a streaming message!!
+ case m := <-stream.Messages:
l.Trace("received message from stream")
- if err := conn.WriteJSON(m); err != nil {
- l.Debugf("error writing json to websocket connection: %s", err)
- // if something is wrong we want to bail and drop the connection -- the client will create a new one
- break sendLoop
+ if err := wsConn.WriteJSON(m); err != nil {
+ l.Debugf("error writing json to websocket connection; breaking off: %s", err)
+ break wsLoop
}
l.Trace("wrote message into websocket connection")
- case <-t.C:
+ case <-streamTicker.C:
l.Trace("received TICK from ticker")
- if err := conn.WriteMessage(websocket.PingMessage, []byte(": ping")); err != nil {
- l.Debugf("error writing ping to websocket connection: %s", err)
- // if something is wrong we want to bail and drop the connection -- the client will create a new one
- break sendLoop
+ if err := wsConn.WriteMessage(websocket.PingMessage, []byte(": ping")); err != nil {
+ l.Debugf("error writing ping to websocket connection; breaking off: %s", err)
+ break wsLoop
}
l.Trace("wrote ping message into websocket connection")
}
}
-
- l.Trace("leaving StreamGETHandler")
}
diff --git a/internal/api/client/timeline/home.go b/internal/api/client/timeline/home.go
index 47bc2943e..db712e4d2 100644
--- a/internal/api/client/timeline/home.go
+++ b/internal/api/client/timeline/home.go
@@ -19,13 +19,13 @@
package timeline
import (
+ "fmt"
"net/http"
"strconv"
- "github.com/sirupsen/logrus"
-
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -105,17 +105,14 @@ import (
// '400':
// description: bad request
func (m *Module) HomeTimelineGETHandler(c *gin.Context) {
- l := logrus.WithField("func", "HomeTimelineGETHandler")
-
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debugf("error authing: %s", err)
- c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
@@ -142,8 +139,8 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) {
if limitString != "" {
i, err := strconv.ParseInt(limitString, 10, 64)
if err != nil {
- l.Debugf("error parsing limit string: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"})
+ err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
limit = int(i)
@@ -154,8 +151,8 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) {
if localString != "" {
i, err := strconv.ParseBool(localString)
if err != nil {
- l.Debugf("error parsing local string: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse local query param"})
+ err := fmt.Errorf("error parsing %s: %s", LocalKey, err)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
local = i
@@ -163,8 +160,7 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) {
resp, errWithCode := m.processor.HomeTimelineGet(c.Request.Context(), authed, maxID, sinceID, minID, limit, local)
if errWithCode != nil {
- l.Debugf("error from processor HomeTimelineGet: %s", errWithCode)
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/timeline/public.go b/internal/api/client/timeline/public.go
index 06da6e947..0cea856c9 100644
--- a/internal/api/client/timeline/public.go
+++ b/internal/api/client/timeline/public.go
@@ -19,13 +19,13 @@
package timeline
import (
+ "fmt"
"net/http"
"strconv"
- "github.com/sirupsen/logrus"
-
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -105,17 +105,14 @@ import (
// '400':
// description: bad request
func (m *Module) PublicTimelineGETHandler(c *gin.Context) {
- l := logrus.WithField("func", "PublicTimelineGETHandler")
-
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debugf("error authing: %s", err)
- c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
@@ -142,8 +139,8 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) {
if limitString != "" {
i, err := strconv.ParseInt(limitString, 10, 64)
if err != nil {
- l.Debugf("error parsing limit string: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"})
+ err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
limit = int(i)
@@ -154,8 +151,8 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) {
if localString != "" {
i, err := strconv.ParseBool(localString)
if err != nil {
- l.Debugf("error parsing local string: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse local query param"})
+ err := fmt.Errorf("error parsing %s: %s", LocalKey, err)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
local = i
@@ -163,8 +160,7 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) {
resp, errWithCode := m.processor.PublicTimelineGet(c.Request.Context(), authed, maxID, sinceID, minID, limit, local)
if errWithCode != nil {
- l.Debugf("error from processor PublicTimelineGet: %s", errWithCode)
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/user/passwordchange.go b/internal/api/client/user/passwordchange.go
index 15b082c1d..7676f5b85 100644
--- a/internal/api/client/user/passwordchange.go
+++ b/internal/api/client/user/passwordchange.go
@@ -19,12 +19,13 @@
package user
import (
+ "errors"
"net/http"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -54,48 +55,48 @@ import (
// responses:
// '200':
// description: Change successful
+// '400':
+// description: bad request
// '401':
// description: unauthorized
// '403':
// description: forbidden
-// '400':
-// description: bad request
+// '406':
+// description: not acceptable
// '500':
-// description: "internal error"
+// description: internal error
func (m *Module) PasswordChangePOSTHandler(c *gin.Context) {
- l := logrus.WithField("func", "PasswordChangePOSTHandler")
-
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
- l.Debugf("error authing: %s", err)
- c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
- return
- }
-
- // First check this user/account is active.
- if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
- l.Debugf("couldn't auth: %s", err)
- c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
form := &model.PasswordChangeRequest{}
- if err := c.ShouldBind(form); err != nil || form == nil || form.NewPassword == "" || form.OldPassword == "" {
- if err != nil {
- l.Debugf("could not parse form from request: %s", err)
- }
- c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"})
+ if err := c.ShouldBind(form); err != nil {
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
+ return
+ }
+
+ if form.OldPassword == "" {
+ err := errors.New("password change request missing field old_password")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
+ return
+ }
+
+ if form.NewPassword == "" {
+ err := errors.New("password change request missing field new_password")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
if errWithCode := m.processor.UserChangePassword(c.Request.Context(), authed, form); errWithCode != nil {
- l.Debugf("error changing user password: %s", errWithCode.Error())
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/client/user/passwordchange_test.go b/internal/api/client/user/passwordchange_test.go
index 23a234116..11df161e2 100644
--- a/internal/api/client/user/passwordchange_test.go
+++ b/internal/api/client/user/passwordchange_test.go
@@ -49,7 +49,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordChangePOST() {
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil)
+ ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil)
ctx.Request.Header.Set("accept", "application/json")
ctx.Request.Form = url.Values{
"old_password": {"password"},
@@ -83,7 +83,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordMissingOldPassword() {
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil)
+ ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil)
ctx.Request.Header.Set("accept", "application/json")
ctx.Request.Form = url.Values{
"new_password": {"peepeepoopoopassword"},
@@ -97,7 +97,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordMissingOldPassword() {
defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body)
suite.NoError(err)
- suite.Equal(`{"error":"missing one or more required form values"}`, string(b))
+ suite.Equal(`{"error":"Bad Request: password change request missing field old_password"}`, string(b))
}
func (suite *PasswordChangeTestSuite) TestPasswordIncorrectOldPassword() {
@@ -110,7 +110,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordIncorrectOldPassword() {
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil)
+ ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil)
ctx.Request.Header.Set("accept", "application/json")
ctx.Request.Form = url.Values{
"old_password": {"notright"},
@@ -125,7 +125,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordIncorrectOldPassword() {
defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body)
suite.NoError(err)
- suite.Equal(`{"error":"bad request: old password did not match"}`, string(b))
+ suite.Equal(`{"error":"Bad Request: old password did not match"}`, string(b))
}
func (suite *PasswordChangeTestSuite) TestPasswordWeakNewPassword() {
@@ -138,7 +138,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordWeakNewPassword() {
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil)
+ ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil)
ctx.Request.Header.Set("accept", "application/json")
ctx.Request.Form = url.Values{
"old_password": {"password"},
@@ -153,7 +153,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordWeakNewPassword() {
defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body)
suite.NoError(err)
- suite.Equal(`{"error":"bad request: password is 94% strength, try including more special characters, using uppercase letters, using numbers or using a longer password"}`, string(b))
+ suite.Equal(`{"error":"Bad Request: password is 94% strength, try including more special characters, using uppercase letters, using numbers or using a longer password"}`, string(b))
}
func TestPasswordChangeTestSuite(t *testing.T) {
diff --git a/internal/api/client/user/user_test.go b/internal/api/client/user/user_test.go
index 6e9c46525..b158ec161 100644
--- a/internal/api/client/user/user_test.go
+++ b/internal/api/client/user/user_test.go
@@ -56,8 +56,8 @@ type UserStandardTestSuite struct {
}
func (suite *UserStandardTestSuite) SetupTest() {
- testrig.InitTestLog()
testrig.InitTestConfig()
+ testrig.InitTestLog()
fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1)
suite.testTokens = testrig.NewTestTokens()
diff --git a/internal/api/errorhandling.go b/internal/api/errorhandling.go
new file mode 100644
index 000000000..57659f83c
--- /dev/null
+++ b/internal/api/errorhandling.go
@@ -0,0 +1,127 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+package api
+
+import (
+ "context"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ "github.com/sirupsen/logrus"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+)
+
+// TODO: add more templated html pages here for different error types
+
+// NotFoundHandler serves a 404 html page through the provided gin context,
+// if accept is 'text/html', or just returns a json error if 'accept' is empty
+// or application/json.
+//
+// When serving html, NotFoundHandler calls the provided InstanceGet function
+// to fetch the apimodel representation of the instance, for serving in the
+// 404 header and footer.
+//
+// If an error is returned by InstanceGet, the function will panic.
+func NotFoundHandler(c *gin.Context, instanceGet func(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode), accept string) {
+ switch accept {
+ case string(TextHTML):
+ host := config.GetHost()
+ instance, err := instanceGet(c.Request.Context(), host)
+ if err != nil {
+ panic(err)
+ }
+
+ c.HTML(http.StatusNotFound, "404.tmpl", gin.H{
+ "instance": instance,
+ })
+ default:
+ c.JSON(http.StatusNotFound, gin.H{"error": http.StatusText(http.StatusNotFound)})
+ }
+}
+
+// genericErrorHandler is a more general version of the NotFoundHandler, which can
+// be used for serving either generic error pages with some rendered help text,
+// or just some error json if the caller prefers (or has no preference).
+func genericErrorHandler(c *gin.Context, instanceGet func(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode), accept string, errWithCode gtserror.WithCode) {
+ switch accept {
+ case string(TextHTML):
+ host := config.GetHost()
+ instance, err := instanceGet(c.Request.Context(), host)
+ if err != nil {
+ panic(err)
+ }
+
+ c.HTML(errWithCode.Code(), "error.tmpl", gin.H{
+ "instance": instance,
+ "code": errWithCode.Code(),
+ "error": errWithCode.Safe(),
+ })
+ default:
+ c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ }
+}
+
+// ErrorHandler takes the provided gin context and errWithCode and tries to serve
+// a helpful error to the caller. It will do content negotiation to figure out if
+// the caller prefers to see an html page with the error rendered there. If not, or
+// if something goes wrong during the function, it will recover and just try to serve
+// an appropriate application/json content-type error.
+func ErrorHandler(c *gin.Context, errWithCode gtserror.WithCode, instanceGet func(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode)) {
+ path := c.Request.URL.Path
+ if raw := c.Request.URL.RawQuery; raw != "" {
+ path = path + "?" + raw
+ }
+
+ l := logrus.WithFields(logrus.Fields{
+ "path": path,
+ "error": errWithCode.Error(),
+ })
+
+ statusCode := errWithCode.Code()
+
+ if statusCode == http.StatusInternalServerError {
+ l.Error("Internal Server Error")
+ } else {
+ l.Debug("handling error")
+ }
+
+ // if we panic for any reason during error handling,
+ // we should still try to return a basic code
+ defer func() {
+ if p := recover(); p != nil {
+ l.Warnf("recovered from panic: %s", p)
+ c.JSON(statusCode, gin.H{"error": errWithCode.Safe()})
+ }
+ }()
+
+ // discover if we're allowed to serve a nice html error page,
+ // or if we should just use a json. Normally we would want to
+ // check for a returned error, but if an error occurs here we
+ // can just fall back to default behavior (serve json error).
+ accept, _ := NegotiateAccept(c, HTMLOrJSONAcceptHeaders...)
+
+ if statusCode == http.StatusNotFound {
+ // use our special not found handler with useful status text
+ NotFoundHandler(c, instanceGet, accept)
+ } else {
+ genericErrorHandler(c, instanceGet, accept, errWithCode)
+ }
+}
diff --git a/internal/gtserror/unauthorized.go b/internal/api/mime.go
similarity index 59%
rename from internal/gtserror/unauthorized.go
rename to internal/api/mime.go
index fb10f748f..0c9595c50 100644
--- a/internal/gtserror/unauthorized.go
+++ b/internal/api/mime.go
@@ -16,4 +16,19 @@
along with this program. If not, see .
*/
-package gtserror
+package api
+
+// MIME represents a mime-type.
+type MIME string
+
+// MIME type
+const (
+ AppJSON MIME = `application/json`
+ AppXML MIME = `application/xml`
+ AppActivityJSON MIME = `application/activity+json`
+ AppActivityLDJSON MIME = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`
+ AppForm MIME = `application/x-www-form-urlencoded`
+ MultipartForm MIME = `multipart/form-data`
+ TextXML MIME = `text/xml`
+ TextHTML MIME = `text/html`
+)
diff --git a/internal/api/negotiate.go b/internal/api/negotiate.go
index 4fb763d9f..510694857 100644
--- a/internal/api/negotiate.go
+++ b/internal/api/negotiate.go
@@ -25,33 +25,40 @@ import (
"github.com/gin-gonic/gin"
)
-// Offer represents an offered mime-type.
-type Offer string
-
-const (
- AppJSON Offer = `application/json` // AppJSON is the mime type for 'application/json'.
- AppActivityJSON Offer = `application/activity+json` // AppActivityJSON is the mime type for 'application/activity+json'.
- AppActivityLDJSON Offer = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"` // AppActivityLDJSON is the mime type for 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
- TextHTML Offer = `text/html` // TextHTML is the mime type for 'text/html'.
-)
-
// ActivityPubAcceptHeaders represents the Accept headers mentioned here:
-// https://www.w3.org/TR/activitypub/#retrieving-objects
-var ActivityPubAcceptHeaders = []Offer{
+//
+var ActivityPubAcceptHeaders = []MIME{
AppActivityJSON,
AppActivityLDJSON,
}
// JSONAcceptHeaders is a slice of offers that just contains application/json types.
-var JSONAcceptHeaders = []Offer{
+var JSONAcceptHeaders = []MIME{
+ AppJSON,
+}
+
+// HTMLOrJSONAcceptHeaders is a slice of offers that prefers TextHTML and will
+// fall back to JSON if necessary. This is useful for error handling, since it can
+// be used to serve a nice HTML page if the caller accepts that, or just JSON if not.
+var HTMLOrJSONAcceptHeaders = []MIME{
+ TextHTML,
AppJSON,
}
// HTMLAcceptHeaders is a slice of offers that just contains text/html types.
-var HTMLAcceptHeaders = []Offer{
+var HTMLAcceptHeaders = []MIME{
TextHTML,
}
+// HTMLOrActivityPubHeaders matches text/html first, then activitypub types.
+// This is useful for user URLs that a user might go to in their browser.
+// https://www.w3.org/TR/activitypub/#retrieving-objects
+var HTMLOrActivityPubHeaders = []MIME{
+ TextHTML,
+ AppActivityJSON,
+ AppActivityLDJSON,
+}
+
// NegotiateAccept takes the *gin.Context from an incoming request, and a
// slice of Offers, and performs content negotiation for the given request
// with the given content-type offers. It will return a string representation
@@ -73,7 +80,7 @@ var HTMLAcceptHeaders = []Offer{
// often-used Accept types.
//
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation#server-driven_content_negotiation
-func NegotiateAccept(c *gin.Context, offers ...Offer) (string, error) {
+func NegotiateAccept(c *gin.Context, offers ...MIME) (string, error) {
if len(offers) == 0 {
return "", errors.New("no format offered")
}
diff --git a/internal/api/s2s/nodeinfo/nodeinfoget.go b/internal/api/s2s/nodeinfo/nodeinfoget.go
index 8b3b1cfba..8f111bd98 100644
--- a/internal/api/s2s/nodeinfo/nodeinfoget.go
+++ b/internal/api/s2s/nodeinfo/nodeinfoget.go
@@ -23,8 +23,8 @@ import (
"net/http"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
// NodeInfoGETHandler swagger:operation GET /nodeinfo/2.0 nodeInfoGet
@@ -45,27 +45,22 @@ import (
// schema:
// "$ref": "#/definitions/nodeinfo"
func (m *Module) NodeInfoGETHandler(c *gin.Context) {
- l := logrus.WithFields(logrus.Fields{
- "func": "NodeInfoGETHandler",
- "user-agent": c.Request.UserAgent(),
- })
-
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
- ni, err := m.processor.GetNodeInfo(c.Request.Context(), c.Request)
+ ni, errWithCode := m.processor.GetNodeInfo(c.Request.Context(), c.Request)
+ if errWithCode != nil {
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
+ return
+ }
+
+ b, err := json.Marshal(ni)
if err != nil {
- l.Debugf("error with get node info request: %s", err)
- c.JSON(err.Code(), err.Safe())
+ api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
return
}
- b, jsonErr := json.Marshal(ni)
- if jsonErr != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": jsonErr.Error()})
- }
-
c.Data(http.StatusOK, `application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"`, b)
}
diff --git a/internal/api/s2s/nodeinfo/wellknownget.go b/internal/api/s2s/nodeinfo/wellknownget.go
index 6fa3c4623..27e18987f 100644
--- a/internal/api/s2s/nodeinfo/wellknownget.go
+++ b/internal/api/s2s/nodeinfo/wellknownget.go
@@ -22,8 +22,8 @@ import (
"net/http"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
// NodeInfoWellKnownGETHandler swagger:operation GET /.well-known/nodeinfo nodeInfoWellKnownGet
@@ -45,19 +45,14 @@ import (
// schema:
// "$ref": "#/definitions/wellKnownResponse"
func (m *Module) NodeInfoWellKnownGETHandler(c *gin.Context) {
- l := logrus.WithFields(logrus.Fields{
- "func": "NodeInfoWellKnownGETHandler",
- })
-
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
- niRel, err := m.processor.GetNodeInfoRel(c.Request.Context(), c.Request)
- if err != nil {
- l.Debugf("error with get node info rel request: %s", err)
- c.JSON(err.Code(), err.Safe())
+ niRel, errWithCode := m.processor.GetNodeInfoRel(c.Request.Context(), c.Request)
+ if errWithCode != nil {
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/s2s/user/followers.go b/internal/api/s2s/user/followers.go
index ce5a03276..77c0e1307 100644
--- a/internal/api/s2s/user/followers.go
+++ b/internal/api/s2s/user/followers.go
@@ -20,48 +20,45 @@ package user
import (
"encoding/json"
- "fmt"
+ "errors"
"net/http"
+ "strings"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
// 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) {
- l := logrus.WithFields(logrus.Fields{
- "func": "FollowersGETHandler",
- "url": c.Request.RequestURI,
- })
-
- requestedUsername := c.Param(UsernameKey)
+ // usernames on our instance are always lowercase
+ requestedUsername := strings.ToLower(c.Param(UsernameKey))
if requestedUsername == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"})
+ err := errors.New("no username specified in request")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
- format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...)
+ format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...)
if err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
- l.Tracef("negotiated format: %s", format)
- ctx := transferContext(c)
+ if format == string(api.TextHTML) {
+ // redirect to the user's profile
+ c.Redirect(http.StatusSeeOther, "/@"+requestedUsername)
+ }
- followers, errWithCode := m.processor.GetFediFollowers(ctx, requestedUsername, c.Request.URL)
+ resp, errWithCode := m.processor.GetFediFollowers(transferContext(c), requestedUsername, c.Request.URL)
if errWithCode != nil {
- l.Info(errWithCode.Error())
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
- b, mErr := json.Marshal(followers)
- if mErr != nil {
- err := fmt.Errorf("could not marshal json: %s", mErr)
- l.Error(err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ b, err := json.Marshal(resp)
+ if err != nil {
+ api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
return
}
diff --git a/internal/api/s2s/user/following.go b/internal/api/s2s/user/following.go
index ca3d4bb4c..14dc9343f 100644
--- a/internal/api/s2s/user/following.go
+++ b/internal/api/s2s/user/following.go
@@ -20,48 +20,45 @@ package user
import (
"encoding/json"
- "fmt"
+ "errors"
"net/http"
+ "strings"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
// 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) {
- l := logrus.WithFields(logrus.Fields{
- "func": "FollowingGETHandler",
- "url": c.Request.RequestURI,
- })
-
- requestedUsername := c.Param(UsernameKey)
+ // usernames on our instance are always lowercase
+ requestedUsername := strings.ToLower(c.Param(UsernameKey))
if requestedUsername == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"})
+ err := errors.New("no username specified in request")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
- format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...)
+ format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...)
if err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
- l.Tracef("negotiated format: %s", format)
- ctx := transferContext(c)
+ if format == string(api.TextHTML) {
+ // redirect to the user's profile
+ c.Redirect(http.StatusSeeOther, "/@"+requestedUsername)
+ }
- following, errWithCode := m.processor.GetFediFollowing(ctx, requestedUsername, c.Request.URL)
+ resp, errWithCode := m.processor.GetFediFollowing(transferContext(c), requestedUsername, c.Request.URL)
if errWithCode != nil {
- l.Info(errWithCode.Error())
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
- b, mErr := json.Marshal(following)
- if mErr != nil {
- err := fmt.Errorf("could not marshal json: %s", mErr)
- l.Error(err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ b, err := json.Marshal(resp)
+ if err != nil {
+ api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
return
}
diff --git a/internal/api/s2s/user/inboxpost.go b/internal/api/s2s/user/inboxpost.go
index 6e17850fd..fa6792cd2 100644
--- a/internal/api/s2s/user/inboxpost.go
+++ b/internal/api/s2s/user/inboxpost.go
@@ -19,43 +19,33 @@
package user
import (
- "net/http"
+ "errors"
+ "strings"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
+ "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/gtserror" //nolint:typecheck
)
// InboxPOSTHandler deals with incoming POST requests to an actor's inbox.
// Eg., POST to https://example.org/users/whatever/inbox.
func (m *Module) InboxPOSTHandler(c *gin.Context) {
- l := logrus.WithFields(logrus.Fields{
- "func": "InboxPOSTHandler",
- "url": c.Request.RequestURI,
- })
-
- requestedUsername := c.Param(UsernameKey)
+ // usernames on our instance are always lowercase
+ requestedUsername := strings.ToLower(c.Param(UsernameKey))
if requestedUsername == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"})
+ err := errors.New("no username specified in request")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
- ctx := transferContext(c)
-
- posted, err := m.processor.InboxPost(ctx, c.Writer, c.Request)
- if err != nil {
+ if posted, err := m.processor.InboxPost(transferContext(c), c.Writer, c.Request); err != nil {
if withCode, ok := err.(gtserror.WithCode); ok {
- l.Debugf("InboxPOSTHandler: %s", withCode.Error())
- c.JSON(withCode.Code(), withCode.Safe())
- return
+ api.ErrorHandler(c, withCode, m.processor.InstanceGet)
+ } else {
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
}
- l.Debugf("InboxPOSTHandler: error processing request: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "unable to process request"})
- return
- }
-
- if !posted {
- l.Debugf("InboxPOSTHandler: request could not be handled as an AP request; headers were: %+v", c.Request.Header)
- c.JSON(http.StatusBadRequest, gin.H{"error": "unable to process request"})
+ } else if !posted {
+ err := errors.New("unable to process request")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
}
}
diff --git a/internal/api/s2s/user/outboxget.go b/internal/api/s2s/user/outboxget.go
index b8e6126df..568e48017 100644
--- a/internal/api/s2s/user/outboxget.go
+++ b/internal/api/s2s/user/outboxget.go
@@ -20,13 +20,15 @@ package user
import (
"encoding/json"
+ "errors"
"fmt"
"net/http"
"strconv"
+ "strings"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
// OutboxGETHandler swagger:operation GET /users/{username}/outbox s2sOutboxGet
@@ -80,23 +82,31 @@ import (
// '404':
// description: not found
func (m *Module) OutboxGETHandler(c *gin.Context) {
- l := logrus.WithFields(logrus.Fields{
- "func": "OutboxGETHandler",
- "url": c.Request.RequestURI,
- })
-
- requestedUsername := c.Param(UsernameKey)
+ // usernames on our instance are always lowercase
+ requestedUsername := strings.ToLower(c.Param(UsernameKey))
if requestedUsername == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"})
+ err := errors.New("no username specified in request")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
+ format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...)
+ if err != nil {
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
+ return
+ }
+
+ if format == string(api.TextHTML) {
+ // redirect to the user's profile
+ c.Redirect(http.StatusSeeOther, "/@"+requestedUsername)
+ }
+
var page bool
if pageString := c.Query(PageKey); pageString != "" {
i, err := strconv.ParseBool(pageString)
if err != nil {
- l.Debugf("error parsing page string: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse page query param"})
+ err := fmt.Errorf("error parsing %s: %s", PageKey, err)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
page = i
@@ -114,27 +124,15 @@ func (m *Module) OutboxGETHandler(c *gin.Context) {
maxID = maxIDString
}
- format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...)
- if err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
- return
- }
- l.Tracef("negotiated format: %s", format)
-
- ctx := transferContext(c)
-
- outbox, errWithCode := m.processor.GetFediOutbox(ctx, requestedUsername, page, maxID, minID, c.Request.URL)
+ resp, errWithCode := m.processor.GetFediOutbox(transferContext(c), requestedUsername, page, maxID, minID, c.Request.URL)
if errWithCode != nil {
- l.Info(errWithCode.Error())
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
- b, mErr := json.Marshal(outbox)
- if mErr != nil {
- err := fmt.Errorf("could not marshal json: %s", mErr)
- l.Error(err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ b, err := json.Marshal(resp)
+ if err != nil {
+ api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
return
}
diff --git a/internal/api/s2s/user/publickeyget.go b/internal/api/s2s/user/publickeyget.go
index 6e963e191..e929c461d 100644
--- a/internal/api/s2s/user/publickeyget.go
+++ b/internal/api/s2s/user/publickeyget.go
@@ -20,12 +20,13 @@ package user
import (
"encoding/json"
- "fmt"
+ "errors"
"net/http"
+ "strings"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
// PublicKeyGETHandler should be served at eg https://example.org/users/:username/main-key.
@@ -34,38 +35,34 @@ import (
// in the form of a vocab.ActivityStreamsPerson. The account will only contain the id,
// public key, username, and type of the account.
func (m *Module) PublicKeyGETHandler(c *gin.Context) {
- l := logrus.WithFields(logrus.Fields{
- "func": "PublicKeyGETHandler",
- "url": c.Request.RequestURI,
- })
-
- requestedUsername := c.Param(UsernameKey)
+ // usernames on our instance are always lowercase
+ requestedUsername := strings.ToLower(c.Param(UsernameKey))
if requestedUsername == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"})
+ err := errors.New("no username specified in request")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
- format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...)
+ format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...)
if err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
- l.Tracef("negotiated format: %s", format)
- ctx := transferContext(c)
+ if format == string(api.TextHTML) {
+ // redirect to the user's profile
+ c.Redirect(http.StatusSeeOther, "/@"+requestedUsername)
+ }
- user, errWithCode := m.processor.GetFediUser(ctx, requestedUsername, c.Request.URL)
+ resp, errWithCode := m.processor.GetFediUser(transferContext(c), requestedUsername, c.Request.URL)
if errWithCode != nil {
- l.Info(errWithCode.Error())
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
- b, mErr := json.Marshal(user)
- if mErr != nil {
- err := fmt.Errorf("could not marshal json: %s", mErr)
- l.Error(err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ b, err := json.Marshal(resp)
+ if err != nil {
+ api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
return
}
diff --git a/internal/api/s2s/user/repliesget.go b/internal/api/s2s/user/repliesget.go
index 388f69885..6ef5a5f43 100644
--- a/internal/api/s2s/user/repliesget.go
+++ b/internal/api/s2s/user/repliesget.go
@@ -20,13 +20,15 @@ package user
import (
"encoding/json"
+ "errors"
"fmt"
"net/http"
"strconv"
+ "strings"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
// StatusRepliesGETHandler swagger:operation GET /users/{username}/statuses/{status}/replies s2sRepliesGet
@@ -86,29 +88,39 @@ import (
// '404':
// description: not found
func (m *Module) StatusRepliesGETHandler(c *gin.Context) {
- l := logrus.WithFields(logrus.Fields{
- "func": "StatusRepliesGETHandler",
- "url": c.Request.RequestURI,
- })
-
- requestedUsername := c.Param(UsernameKey)
+ // usernames on our instance are always lowercase
+ requestedUsername := strings.ToLower(c.Param(UsernameKey))
if requestedUsername == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"})
+ err := errors.New("no username specified in request")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
- requestedStatusID := c.Param(StatusIDKey)
+ // status IDs on our instance are always uppercase
+ requestedStatusID := strings.ToUpper(c.Param(StatusIDKey))
if requestedStatusID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no status id specified in request"})
+ err := errors.New("no status id specified in request")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
+ format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...)
+ if err != nil {
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
+ return
+ }
+
+ if format == string(api.TextHTML) {
+ // redirect to the status
+ c.Redirect(http.StatusSeeOther, "/@"+requestedUsername+"/statuses/"+requestedStatusID)
+ }
+
var page bool
if pageString := c.Query(PageKey); pageString != "" {
i, err := strconv.ParseBool(pageString)
if err != nil {
- l.Debugf("error parsing page string: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse page query param"})
+ err := fmt.Errorf("error parsing %s: %s", PageKey, err)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
page = i
@@ -119,8 +131,8 @@ func (m *Module) StatusRepliesGETHandler(c *gin.Context) {
if onlyOtherAccountsString != "" {
i, err := strconv.ParseBool(onlyOtherAccountsString)
if err != nil {
- l.Debugf("error parsing only_other_accounts string: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse only_other_accounts query param"})
+ err := fmt.Errorf("error parsing %s: %s", OnlyOtherAccountsKey, err)
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
onlyOtherAccounts = i
@@ -132,27 +144,15 @@ func (m *Module) StatusRepliesGETHandler(c *gin.Context) {
minID = minIDString
}
- format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...)
- if err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
- return
- }
- l.Tracef("negotiated format: %s", format)
-
- ctx := transferContext(c)
-
- replies, errWithCode := m.processor.GetFediStatusReplies(ctx, requestedUsername, requestedStatusID, page, onlyOtherAccounts, minID, c.Request.URL)
+ resp, errWithCode := m.processor.GetFediStatusReplies(transferContext(c), requestedUsername, requestedStatusID, page, onlyOtherAccounts, minID, c.Request.URL)
if errWithCode != nil {
- l.Info(errWithCode.Error())
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
- b, mErr := json.Marshal(replies)
- if mErr != nil {
- err := fmt.Errorf("could not marshal json: %s", mErr)
- l.Error(err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ b, err := json.Marshal(resp)
+ if err != nil {
+ api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
return
}
diff --git a/internal/api/s2s/user/statusget.go b/internal/api/s2s/user/statusget.go
index 3dee0c88a..b6690c4de 100644
--- a/internal/api/s2s/user/statusget.go
+++ b/internal/api/s2s/user/statusget.go
@@ -20,57 +20,53 @@ package user
import (
"encoding/json"
- "fmt"
+ "errors"
"net/http"
"strings"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
// StatusGETHandler serves the target status as an activitystreams NOTE so that other AP servers can parse it.
func (m *Module) StatusGETHandler(c *gin.Context) {
- l := logrus.WithFields(logrus.Fields{
- "func": "StatusGETHandler",
- "url": c.Request.RequestURI,
- })
-
// usernames on our instance are always lowercase
requestedUsername := strings.ToLower(c.Param(UsernameKey))
if requestedUsername == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"})
+ err := errors.New("no username specified in request")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
// status IDs on our instance are always uppercase
requestedStatusID := strings.ToUpper(c.Param(StatusIDKey))
if requestedStatusID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no status id specified in request"})
+ err := errors.New("no status id specified in request")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
- format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...)
+ format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...)
if err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
- l.Tracef("negotiated format: %s", format)
- ctx := transferContext(c)
+ if format == string(api.TextHTML) {
+ // redirect to the status
+ c.Redirect(http.StatusSeeOther, "/@"+requestedUsername+"/statuses/"+requestedStatusID)
+ }
- status, errWithCode := m.processor.GetFediStatus(ctx, requestedUsername, requestedStatusID, c.Request.URL)
+ resp, errWithCode := m.processor.GetFediStatus(transferContext(c), requestedUsername, requestedStatusID, c.Request.URL)
if errWithCode != nil {
- l.Info(errWithCode.Error())
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
- b, mErr := json.Marshal(status)
- if mErr != nil {
- err := fmt.Errorf("could not marshal json: %s", mErr)
- l.Error(err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ b, err := json.Marshal(resp)
+ if err != nil {
+ api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
return
}
diff --git a/internal/api/s2s/user/userget.go b/internal/api/s2s/user/userget.go
index c034e6bfc..b5e8a84c9 100644
--- a/internal/api/s2s/user/userget.go
+++ b/internal/api/s2s/user/userget.go
@@ -20,12 +20,13 @@ package user
import (
"encoding/json"
- "fmt"
+ "errors"
"net/http"
+ "strings"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
// UsersGETHandler should be served at https://example.org/users/:username.
@@ -38,38 +39,34 @@ import (
// And of course, the request should be refused if the account or server making the
// request is blocked.
func (m *Module) UsersGETHandler(c *gin.Context) {
- l := logrus.WithFields(logrus.Fields{
- "func": "UsersGETHandler",
- "url": c.Request.RequestURI,
- })
-
- requestedUsername := c.Param(UsernameKey)
+ // usernames on our instance are always lowercase
+ requestedUsername := strings.ToLower(c.Param(UsernameKey))
if requestedUsername == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"})
+ err := errors.New("no username specified in request")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
- format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...)
+ format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...)
if err != nil {
- c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}
- l.Tracef("negotiated format: %s", format)
- ctx := transferContext(c)
+ if format == string(api.TextHTML) {
+ // redirect to the user's profile
+ c.Redirect(http.StatusSeeOther, "/@"+requestedUsername)
+ }
- user, errWithCode := m.processor.GetFediUser(ctx, requestedUsername, c.Request.URL) // GetFediUser handles auth as well
+ resp, errWithCode := m.processor.GetFediUser(transferContext(c), requestedUsername, c.Request.URL)
if errWithCode != nil {
- l.Info(errWithCode.Error())
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
- b, mErr := json.Marshal(user)
- if mErr != nil {
- err := fmt.Errorf("could not marshal json: %s", mErr)
- l.Error(err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ b, err := json.Marshal(resp)
+ if err != nil {
+ api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
return
}
diff --git a/internal/api/s2s/webfinger/webfingerget.go b/internal/api/s2s/webfinger/webfingerget.go
index a271c031b..7e7ca006b 100644
--- a/internal/api/s2s/webfinger/webfingerget.go
+++ b/internal/api/s2s/webfinger/webfingerget.go
@@ -27,6 +27,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/ap"
+ "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/config"
)
@@ -105,10 +106,9 @@ func (m *Module) WebfingerGETRequest(c *gin.Context) {
ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeyVerifier, verifier)
}
- resp, err := m.processor.GetWebfingerAccount(ctx, username)
- if err != nil {
- l.Debugf("aborting request with an error: %s", err.Error())
- c.JSON(err.Code(), gin.H{"error": err.Safe()})
+ resp, errWithCode := m.processor.GetWebfingerAccount(ctx, username)
+ if errWithCode != nil {
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
diff --git a/internal/api/security/extraheaders.go b/internal/api/security/extraheaders.go
index bcb41e0c0..f66ac43b4 100644
--- a/internal/api/security/extraheaders.go
+++ b/internal/api/security/extraheaders.go
@@ -1,3 +1,21 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
package security
import "github.com/gin-gonic/gin"
diff --git a/internal/api/security/useragentblock.go b/internal/api/security/useragentblock.go
index acbdeae58..b117e8608 100644
--- a/internal/api/security/useragentblock.go
+++ b/internal/api/security/useragentblock.go
@@ -19,21 +19,17 @@
package security
import (
+ "errors"
"net/http"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
)
-// UserAgentBlock blocks requests with undesired, empty, or invalid user-agent strings.
+// UserAgentBlock aborts requests with empty user agent strings.
func (m *Module) UserAgentBlock(c *gin.Context) {
- l := logrus.WithFields(logrus.Fields{
- "func": "UserAgentBlock",
- })
-
if ua := c.Request.UserAgent(); ua == "" {
- l.Debug("aborting request because there's no user-agent set")
- c.AbortWithStatus(http.StatusTeapot)
- return
+ code := http.StatusTeapot
+ err := errors.New(http.StatusText(code) + ": no user-agent sent with request")
+ c.AbortWithStatusJSON(code, gin.H{"error": err.Error()})
}
}
diff --git a/internal/federation/authenticate.go b/internal/federation/authenticate.go
index 1d0a4a7ef..5ca2f9806 100644
--- a/internal/federation/authenticate.go
+++ b/internal/federation/authenticate.go
@@ -126,7 +126,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
vi := ctx.Value(ap.ContextRequestingPublicKeyVerifier)
if vi == nil {
err := errors.New("http request wasn't signed or http signature was invalid")
- errWithCode := gtserror.NewErrorNotAuthorized(err, err.Error())
+ errWithCode := gtserror.NewErrorUnauthorized(err, err.Error())
l.Debug(errWithCode)
return nil, errWithCode
}
@@ -134,7 +134,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
verifier, ok := vi.(httpsig.Verifier)
if !ok {
err := errors.New("http request wasn't signed or http signature was invalid")
- errWithCode := gtserror.NewErrorNotAuthorized(err, err.Error())
+ errWithCode := gtserror.NewErrorUnauthorized(err, err.Error())
l.Debug(errWithCode)
return nil, errWithCode
}
@@ -143,7 +143,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
si := ctx.Value(ap.ContextRequestingPublicKeySignature)
if si == nil {
err := errors.New("http request wasn't signed or http signature was invalid")
- errWithCode := gtserror.NewErrorNotAuthorized(err, err.Error())
+ errWithCode := gtserror.NewErrorUnauthorized(err, err.Error())
l.Debug(errWithCode)
return nil, errWithCode
}
@@ -151,7 +151,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
signature, ok := si.(string)
if !ok {
err := errors.New("http request wasn't signed or http signature was invalid")
- errWithCode := gtserror.NewErrorNotAuthorized(err, err.Error())
+ errWithCode := gtserror.NewErrorUnauthorized(err, err.Error())
l.Debug(errWithCode)
return nil, errWithCode
}
@@ -209,7 +209,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
// The actual http call to the remote server is made right here in the Dereference function.
b, err := transport.Dereference(ctx, requestingPublicKeyID)
if err != nil {
- errWithCode := gtserror.NewErrorNotAuthorized(fmt.Errorf("error dereferencing public key %s: %s", requestingPublicKeyID, err))
+ errWithCode := gtserror.NewErrorUnauthorized(fmt.Errorf("error dereferencing public key %s: %s", requestingPublicKeyID, err))
l.Debug(errWithCode)
return nil, errWithCode
}
@@ -217,7 +217,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
// if the key isn't in the response, we can't authenticate the request
requestingPublicKey, err := getPublicKeyFromResponse(ctx, b, requestingPublicKeyID)
if err != nil {
- errWithCode := gtserror.NewErrorNotAuthorized(fmt.Errorf("error parsing public key %s: %s", requestingPublicKeyID, err))
+ errWithCode := gtserror.NewErrorUnauthorized(fmt.Errorf("error parsing public key %s: %s", requestingPublicKeyID, err))
l.Debug(errWithCode)
return nil, errWithCode
}
@@ -225,7 +225,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
// we should be able to get the actual key embedded in the vocab.W3IDSecurityV1PublicKey
pkPemProp := requestingPublicKey.GetW3IDSecurityV1PublicKeyPem()
if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() {
- errWithCode := gtserror.NewErrorNotAuthorized(errors.New("publicKeyPem property is not provided or it is not embedded as a value"))
+ errWithCode := gtserror.NewErrorUnauthorized(errors.New("publicKeyPem property is not provided or it is not embedded as a value"))
l.Debug(errWithCode)
return nil, errWithCode
}
@@ -234,14 +234,14 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
pubKeyPem := pkPemProp.Get()
block, _ := pem.Decode([]byte(pubKeyPem))
if block == nil || block.Type != "PUBLIC KEY" {
- errWithCode := gtserror.NewErrorNotAuthorized(errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type"))
+ errWithCode := gtserror.NewErrorUnauthorized(errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type"))
l.Debug(errWithCode)
return nil, errWithCode
}
publicKey, err = x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
- errWithCode := gtserror.NewErrorNotAuthorized(fmt.Errorf("could not parse public key %s from block bytes: %s", requestingPublicKeyID, err))
+ errWithCode := gtserror.NewErrorUnauthorized(fmt.Errorf("could not parse public key %s from block bytes: %s", requestingPublicKeyID, err))
l.Debug(errWithCode)
return nil, errWithCode
}
@@ -249,7 +249,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
// all good! we just need the URI of the key owner to return
pkOwnerProp := requestingPublicKey.GetW3IDSecurityV1Owner()
if pkOwnerProp == nil || !pkOwnerProp.IsIRI() {
- errWithCode := gtserror.NewErrorNotAuthorized(errors.New("publicKeyOwner property is not provided or it is not embedded as a value"))
+ errWithCode := gtserror.NewErrorUnauthorized(errors.New("publicKeyOwner property is not provided or it is not embedded as a value"))
l.Debug(errWithCode)
return nil, errWithCode
}
@@ -280,7 +280,7 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
l.Tracef("authentication for %s NOT PASSED with algorithm %s: %s", pkOwnerURI, algo, err)
}
- errWithCode := gtserror.NewErrorNotAuthorized(fmt.Errorf("authentication not passed for public key owner %s; signature value was '%s'", pkOwnerURI, signature))
+ errWithCode := gtserror.NewErrorUnauthorized(fmt.Errorf("authentication not passed for public key owner %s; signature value was '%s'", pkOwnerURI, signature))
l.Debug(errWithCode)
return nil, errWithCode
}
diff --git a/internal/gtserror/withcode.go b/internal/gtserror/withcode.go
index 34889b961..6672000dc 100644
--- a/internal/gtserror/withcode.go
+++ b/internal/gtserror/withcode.go
@@ -60,7 +60,7 @@ func (e withCode) Code() int {
// NewErrorBadRequest returns an ErrorWithCode 400 with the given original error and optional help text.
func NewErrorBadRequest(original error, helpText ...string) WithCode {
- safe := "bad request"
+ safe := http.StatusText(http.StatusBadRequest)
if helpText != nil {
safe = safe + ": " + strings.Join(helpText, ": ")
}
@@ -71,9 +71,9 @@ func NewErrorBadRequest(original error, helpText ...string) WithCode {
}
}
-// NewErrorNotAuthorized returns an ErrorWithCode 401 with the given original error and optional help text.
-func NewErrorNotAuthorized(original error, helpText ...string) WithCode {
- safe := "not authorized"
+// NewErrorUnauthorized returns an ErrorWithCode 401 with the given original error and optional help text.
+func NewErrorUnauthorized(original error, helpText ...string) WithCode {
+ safe := http.StatusText(http.StatusUnauthorized)
if helpText != nil {
safe = safe + ": " + strings.Join(helpText, ": ")
}
@@ -86,7 +86,7 @@ func NewErrorNotAuthorized(original error, helpText ...string) WithCode {
// NewErrorForbidden returns an ErrorWithCode 403 with the given original error and optional help text.
func NewErrorForbidden(original error, helpText ...string) WithCode {
- safe := "forbidden"
+ safe := http.StatusText(http.StatusForbidden)
if helpText != nil {
safe = safe + ": " + strings.Join(helpText, ": ")
}
@@ -99,7 +99,7 @@ func NewErrorForbidden(original error, helpText ...string) WithCode {
// NewErrorNotFound returns an ErrorWithCode 404 with the given original error and optional help text.
func NewErrorNotFound(original error, helpText ...string) WithCode {
- safe := "404 not found"
+ safe := http.StatusText(http.StatusNotFound)
if helpText != nil {
safe = safe + ": " + strings.Join(helpText, ": ")
}
@@ -112,7 +112,7 @@ func NewErrorNotFound(original error, helpText ...string) WithCode {
// NewErrorInternalError returns an ErrorWithCode 500 with the given original error and optional help text.
func NewErrorInternalError(original error, helpText ...string) WithCode {
- safe := "internal server error"
+ safe := http.StatusText(http.StatusInternalServerError)
if helpText != nil {
safe = safe + ": " + strings.Join(helpText, ": ")
}
@@ -125,7 +125,7 @@ func NewErrorInternalError(original error, helpText ...string) WithCode {
// NewErrorConflict returns an ErrorWithCode 409 with the given original error and optional help text.
func NewErrorConflict(original error, helpText ...string) WithCode {
- safe := "conflict"
+ safe := http.StatusText(http.StatusConflict)
if helpText != nil {
safe = safe + ": " + strings.Join(helpText, ": ")
}
@@ -135,3 +135,29 @@ func NewErrorConflict(original error, helpText ...string) WithCode {
code: http.StatusConflict,
}
}
+
+// NewErrorNotAcceptable returns an ErrorWithCode 406 with the given original error and optional help text.
+func NewErrorNotAcceptable(original error, helpText ...string) WithCode {
+ safe := http.StatusText(http.StatusNotAcceptable)
+ if helpText != nil {
+ safe = safe + ": " + strings.Join(helpText, ": ")
+ }
+ return withCode{
+ original: original,
+ safe: errors.New(safe),
+ code: http.StatusNotAcceptable,
+ }
+}
+
+// NewErrorUnprocessableEntity returns an ErrorWithCode 422 with the given original error and optional help text.
+func NewErrorUnprocessableEntity(original error, helpText ...string) WithCode {
+ safe := http.StatusText(http.StatusUnprocessableEntity)
+ if helpText != nil {
+ safe = safe + ": " + strings.Join(helpText, ": ")
+ }
+ return withCode{
+ original: original,
+ safe: errors.New(safe),
+ code: http.StatusUnprocessableEntity,
+ }
+}
diff --git a/internal/oidc/handlecallback.go b/internal/oidc/handlecallback.go
index c0c1ed453..588fb227b 100644
--- a/internal/oidc/handlecallback.go
+++ b/internal/oidc/handlecallback.go
@@ -24,24 +24,28 @@ import (
"fmt"
"github.com/sirupsen/logrus"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
-func (i *idp) HandleCallback(ctx context.Context, code string) (*Claims, error) {
+func (i *idp) HandleCallback(ctx context.Context, code string) (*Claims, gtserror.WithCode) {
l := logrus.WithField("func", "HandleCallback")
if code == "" {
- return nil, errors.New("code was empty string")
+ err := errors.New("code was empty string")
+ return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
l.Debug("exchanging code for oauth2token")
oauth2Token, err := i.oauth2Config.Exchange(ctx, code)
if err != nil {
- return nil, fmt.Errorf("error exchanging code for oauth2token: %s", err)
+ err := fmt.Errorf("error exchanging code for oauth2token: %s", err)
+ return nil, gtserror.NewErrorInternalError(err)
}
l.Debug("extracting id_token")
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
- return nil, errors.New("no id_token in oauth2token")
+ err := errors.New("no id_token in oauth2token")
+ return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
l.Debugf("raw id token: %s", rawIDToken)
@@ -50,13 +54,15 @@ func (i *idp) HandleCallback(ctx context.Context, code string) (*Claims, error)
idTokenVerifier := i.provider.Verifier(i.oidcConf)
idToken, err := idTokenVerifier.Verify(ctx, rawIDToken)
if err != nil {
- return nil, fmt.Errorf("could not verify id token: %s", err)
+ err = fmt.Errorf("could not verify id token: %s", err)
+ return nil, gtserror.NewErrorUnauthorized(err, err.Error())
}
l.Debug("extracting claims from id_token")
claims := &Claims{}
if err := idToken.Claims(claims); err != nil {
- return nil, fmt.Errorf("could not parse claims from idToken: %s", err)
+ err := fmt.Errorf("could not parse claims from idToken: %s", err)
+ return nil, gtserror.NewErrorInternalError(err, err.Error())
}
return claims, nil
diff --git a/internal/oidc/idp.go b/internal/oidc/idp.go
index 7ce535644..90aee81f4 100644
--- a/internal/oidc/idp.go
+++ b/internal/oidc/idp.go
@@ -24,6 +24,7 @@ import (
"github.com/coreos/go-oidc/v3/oidc"
"github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"golang.org/x/oauth2"
)
@@ -39,7 +40,7 @@ type IDP interface {
// with a set of claims.
//
// Note that this function *does not* verify state. That should be handled by the caller *before* this function is called.
- HandleCallback(ctx context.Context, code string) (*Claims, error)
+ HandleCallback(ctx context.Context, code string) (*Claims, gtserror.WithCode)
// AuthCodeURL returns the proper redirect URL for this IDP, for redirecting requesters to the correct OIDC endpoint.
AuthCodeURL(state string) string
}
diff --git a/internal/processing/account.go b/internal/processing/account.go
index 43c4f65e6..2c21aee2e 100644
--- a/internal/processing/account.go
+++ b/internal/processing/account.go
@@ -26,7 +26,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
-func (p *processor) AccountCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountCreateRequest) (*apimodel.Token, error) {
+func (p *processor) AccountCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountCreateRequest) (*apimodel.Token, gtserror.WithCode) {
return p.accountProcessor.Create(ctx, authed.Token, authed.Application, form)
}
@@ -42,7 +42,7 @@ func (p *processor) AccountGetLocalByUsername(ctx context.Context, authed *oauth
return p.accountProcessor.GetLocalByUsername(ctx, authed.Account, username)
}
-func (p *processor) AccountUpdate(ctx context.Context, authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) {
+func (p *processor) AccountUpdate(ctx context.Context, authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode) {
return p.accountProcessor.Update(ctx, authed.Account, form)
}
diff --git a/internal/processing/account/account.go b/internal/processing/account/account.go
index 2a27fc743..56dfb90e9 100644
--- a/internal/processing/account/account.go
+++ b/internal/processing/account/account.go
@@ -40,7 +40,7 @@ import (
// Processor wraps a bunch of functions for processing account actions.
type Processor interface {
// Create processes the given form for creating a new account, returning an oauth token for that account if successful.
- Create(ctx context.Context, applicationToken oauth2.TokenInfo, application *gtsmodel.Application, form *apimodel.AccountCreateRequest) (*apimodel.Token, error)
+ Create(ctx context.Context, applicationToken oauth2.TokenInfo, application *gtsmodel.Application, form *apimodel.AccountCreateRequest) (*apimodel.Token, gtserror.WithCode)
// Delete deletes an account, and all of that account's statuses, media, follows, notifications, etc etc etc.
// The origin passed here should be either the ID of the account doing the delete (can be itself), or the ID of a domain block.
Delete(ctx context.Context, account *gtsmodel.Account, origin string) gtserror.WithCode
@@ -52,7 +52,7 @@ type Processor interface {
// GetLocalByUsername processes the given request for account information targeting a local account by username.
GetLocalByUsername(ctx context.Context, requestingAccount *gtsmodel.Account, username string) (*apimodel.Account, gtserror.WithCode)
// Update processes the update of an account with the given form
- Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error)
+ Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode)
// StatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for
// the account given in authed.
StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) (*apimodel.TimelineResponse, gtserror.WithCode)
diff --git a/internal/processing/account/create.go b/internal/processing/account/create.go
index 72626220b..44d6bbea5 100644
--- a/internal/processing/account/create.go
+++ b/internal/processing/account/create.go
@@ -27,29 +27,30 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/ap"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/text"
"github.com/superseriousbusiness/oauth2/v4"
)
-func (p *processor) Create(ctx context.Context, applicationToken oauth2.TokenInfo, application *gtsmodel.Application, form *apimodel.AccountCreateRequest) (*apimodel.Token, error) {
+func (p *processor) Create(ctx context.Context, applicationToken oauth2.TokenInfo, application *gtsmodel.Application, form *apimodel.AccountCreateRequest) (*apimodel.Token, gtserror.WithCode) {
l := logrus.WithField("func", "accountCreate")
emailAvailable, err := p.db.IsEmailAvailable(ctx, form.Email)
if err != nil {
- return nil, err
+ return nil, gtserror.NewErrorBadRequest(err)
}
if !emailAvailable {
- return nil, fmt.Errorf("email address %s in use", form.Email)
+ return nil, gtserror.NewErrorConflict(fmt.Errorf("email address %s is not available", form.Email))
}
usernameAvailable, err := p.db.IsUsernameAvailable(ctx, form.Username)
if err != nil {
- return nil, err
+ return nil, gtserror.NewErrorBadRequest(err)
}
if !usernameAvailable {
- return nil, fmt.Errorf("username %s in use", form.Username)
+ return nil, gtserror.NewErrorConflict(fmt.Errorf("username %s in use", form.Username))
}
reasonRequired := config.GetAccountsReasonRequired()
@@ -64,19 +65,19 @@ func (p *processor) Create(ctx context.Context, applicationToken oauth2.TokenInf
l.Trace("creating new username and account")
user, err := p.db.NewSignup(ctx, form.Username, text.SanitizePlaintext(reason), approvalRequired, form.Email, form.Password, form.IP, form.Locale, application.ID, false, false)
if err != nil {
- return nil, fmt.Errorf("error creating new signup in the database: %s", err)
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error creating new signup in the database: %s", err))
}
l.Tracef("generating a token for user %s with account %s and application %s", user.ID, user.AccountID, application.ID)
accessToken, err := p.oauthServer.GenerateUserAccessToken(ctx, applicationToken, application.ClientSecret, user.ID)
if err != nil {
- return nil, fmt.Errorf("error creating new access token for user %s: %s", user.ID, err)
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error creating new access token for user %s: %s", user.ID, err))
}
if user.Account == nil {
a, err := p.db.GetAccountByID(ctx, user.AccountID)
if err != nil {
- return nil, fmt.Errorf("error getting new account from the database: %s", err)
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error getting new account from the database: %s", err))
}
user.Account = a
}
diff --git a/internal/processing/account/get.go b/internal/processing/account/get.go
index 97f2f0b4a..70c1cd9fe 100644
--- a/internal/processing/account/get.go
+++ b/internal/processing/account/get.go
@@ -94,5 +94,6 @@ func (p *processor) getAccountFor(ctx context.Context, requestingAccount *gtsmod
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting account: %s", err))
}
+
return apiAccount, nil
}
diff --git a/internal/processing/account/update.go b/internal/processing/account/update.go
index 42da40ffe..d1085845b 100644
--- a/internal/processing/account/update.go
+++ b/internal/processing/account/update.go
@@ -29,6 +29,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/ap"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/messages"
@@ -37,7 +38,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/validate"
)
-func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) {
+func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode) {
l := logrus.WithField("func", "AccountUpdate")
if form.Discoverable != nil {
@@ -50,14 +51,14 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form
if form.DisplayName != nil {
if err := validate.DisplayName(*form.DisplayName); err != nil {
- return nil, err
+ return nil, gtserror.NewErrorBadRequest(err)
}
account.DisplayName = text.SanitizePlaintext(*form.DisplayName)
}
if form.Note != nil {
if err := validate.Note(*form.Note); err != nil {
- return nil, err
+ return nil, gtserror.NewErrorBadRequest(err)
}
// Set the raw note before processing
@@ -66,7 +67,7 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form
// Process note to generate a valid HTML representation
note, err := p.processNote(ctx, *form.Note, account.ID)
if err != nil {
- return nil, err
+ return nil, gtserror.NewErrorBadRequest(err)
}
// Set updated HTML-ified note
@@ -76,7 +77,7 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form
if form.Avatar != nil && form.Avatar.Size != 0 {
avatarInfo, err := p.UpdateAvatar(ctx, form.Avatar, account.ID)
if err != nil {
- return nil, err
+ return nil, gtserror.NewErrorBadRequest(err)
}
account.AvatarMediaAttachmentID = avatarInfo.ID
account.AvatarMediaAttachment = avatarInfo
@@ -86,7 +87,7 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form
if form.Header != nil && form.Header.Size != 0 {
headerInfo, err := p.UpdateHeader(ctx, form.Header, account.ID)
if err != nil {
- return nil, err
+ return nil, gtserror.NewErrorBadRequest(err)
}
account.HeaderMediaAttachmentID = headerInfo.ID
account.HeaderMediaAttachment = headerInfo
@@ -100,7 +101,7 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form
if form.Source != nil {
if form.Source.Language != nil {
if err := validate.Language(*form.Source.Language); err != nil {
- return nil, err
+ return nil, gtserror.NewErrorBadRequest(err)
}
account.Language = *form.Source.Language
}
@@ -111,7 +112,7 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form
if form.Source.Privacy != nil {
if err := validate.Privacy(*form.Source.Privacy); err != nil {
- return nil, err
+ return nil, gtserror.NewErrorBadRequest(err)
}
privacy := p.tc.APIVisToVis(apimodel.Visibility(*form.Source.Privacy))
account.Privacy = privacy
@@ -120,7 +121,7 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form
updatedAccount, err := p.db.UpdateAccount(ctx, account)
if err != nil {
- return nil, fmt.Errorf("could not update account %s: %s", account.ID, err)
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("could not update account %s: %s", account.ID, err))
}
p.clientWorker.Queue(messages.FromClientAPI{
@@ -132,7 +133,7 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form
acctSensitive, err := p.tc.AccountToAPIAccountSensitive(ctx, updatedAccount)
if err != nil {
- return nil, fmt.Errorf("could not convert account into apisensitive account: %s", err)
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("could not convert account into apisensitive account: %s", err))
}
return acctSensitive, nil
}
diff --git a/internal/processing/account/update_test.go b/internal/processing/account/update_test.go
index 9f9b6cb77..582dc82e9 100644
--- a/internal/processing/account/update_test.go
+++ b/internal/processing/account/update_test.go
@@ -45,8 +45,8 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateSimple() {
}
// should get no error from the update function, and an api model account returned
- apiAccount, err := suite.accountProcessor.Update(context.Background(), testAccount, form)
- suite.NoError(err)
+ apiAccount, errWithCode := suite.accountProcessor.Update(context.Background(), testAccount, form)
+ suite.NoError(errWithCode)
suite.NotNil(apiAccount)
// fields on the profile should be updated
@@ -88,8 +88,8 @@ go check out @1happyturtle, they have a cool account!
}
// should get no error from the update function, and an api model account returned
- apiAccount, err := suite.accountProcessor.Update(context.Background(), testAccount, form)
- suite.NoError(err)
+ apiAccount, errWithCode := suite.accountProcessor.Update(context.Background(), testAccount, form)
+ suite.NoError(errWithCode)
suite.NotNil(apiAccount)
// fields on the profile should be updated
diff --git a/internal/processing/admin/emoji.go b/internal/processing/admin/emoji.go
index 78920273c..f91f972a8 100644
--- a/internal/processing/admin/emoji.go
+++ b/internal/processing/admin/emoji.go
@@ -34,7 +34,7 @@ import (
func (p *processor) EmojiCreate(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, gtserror.WithCode) {
if !user.Admin {
- return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("user %s not an admin", user.ID), "user is not an admin")
+ return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("user %s not an admin", user.ID), "user is not an admin")
}
data := func(innerCtx context.Context) (io.Reader, int, error) {
diff --git a/internal/processing/app.go b/internal/processing/app.go
index f0d8755c1..2e13c07b9 100644
--- a/internal/processing/app.go
+++ b/internal/processing/app.go
@@ -23,12 +23,13 @@ import (
"github.com/google/uuid"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
-func (p *processor) AppCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, error) {
+func (p *processor) AppCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, gtserror.WithCode) {
// set default 'read' for scopes if it's not set
var scopes string
if form.Scopes == "" {
@@ -40,13 +41,13 @@ func (p *processor) AppCreate(ctx context.Context, authed *oauth.Auth, form *api
// generate new IDs for this application and its associated client
clientID, err := id.NewRandomULID()
if err != nil {
- return nil, err
+ return nil, gtserror.NewErrorInternalError(err)
}
clientSecret := uuid.NewString()
appID, err := id.NewRandomULID()
if err != nil {
- return nil, err
+ return nil, gtserror.NewErrorInternalError(err)
}
// generate the application to put in the database
@@ -62,7 +63,7 @@ func (p *processor) AppCreate(ctx context.Context, authed *oauth.Auth, form *api
// chuck it in the db
if err := p.db.Put(ctx, app); err != nil {
- return nil, err
+ return nil, gtserror.NewErrorInternalError(err)
}
// now we need to model an oauth client from the application that the oauth library can use
@@ -70,17 +71,18 @@ func (p *processor) AppCreate(ctx context.Context, authed *oauth.Auth, form *api
ID: clientID,
Secret: clientSecret,
Domain: form.RedirectURIs,
- UserID: "", // This client isn't yet associated with a specific user, it's just an app client right now
+ // This client isn't yet associated with a specific user, it's just an app client right now
+ UserID: "",
}
// chuck it in the db
if err := p.db.Put(ctx, oc); err != nil {
- return nil, err
+ return nil, gtserror.NewErrorInternalError(err)
}
apiApp, err := p.tc.AppToAPIAppSensitive(ctx, app)
if err != nil {
- return nil, err
+ return nil, gtserror.NewErrorInternalError(err)
}
return apiApp, nil
diff --git a/internal/processing/federation/getfollowers.go b/internal/processing/federation/getfollowers.go
index a49037397..6c54dbd50 100644
--- a/internal/processing/federation/getfollowers.go
+++ b/internal/processing/federation/getfollowers.go
@@ -42,7 +42,7 @@ func (p *processor) GetFollowers(ctx context.Context, requestedUsername string,
requestingAccount, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false, false)
if err != nil {
- return nil, gtserror.NewErrorNotAuthorized(err)
+ return nil, gtserror.NewErrorUnauthorized(err)
}
blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true)
@@ -51,7 +51,7 @@ func (p *processor) GetFollowers(ctx context.Context, requestedUsername string,
}
if blocked {
- return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
+ return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
}
requestedAccountURI, err := url.Parse(requestedAccount.URI)
diff --git a/internal/processing/federation/getfollowing.go b/internal/processing/federation/getfollowing.go
index a38c049fd..6b1afae92 100644
--- a/internal/processing/federation/getfollowing.go
+++ b/internal/processing/federation/getfollowing.go
@@ -42,7 +42,7 @@ func (p *processor) GetFollowing(ctx context.Context, requestedUsername string,
requestingAccount, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false, false)
if err != nil {
- return nil, gtserror.NewErrorNotAuthorized(err)
+ return nil, gtserror.NewErrorUnauthorized(err)
}
blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true)
@@ -51,7 +51,7 @@ func (p *processor) GetFollowing(ctx context.Context, requestedUsername string,
}
if blocked {
- return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
+ return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
}
requestedAccountURI, err := url.Parse(requestedAccount.URI)
diff --git a/internal/processing/federation/getoutbox.go b/internal/processing/federation/getoutbox.go
index 455f427f3..4e428c1ae 100644
--- a/internal/processing/federation/getoutbox.go
+++ b/internal/processing/federation/getoutbox.go
@@ -43,7 +43,7 @@ func (p *processor) GetOutbox(ctx context.Context, requestedUsername string, pag
requestingAccount, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false, false)
if err != nil {
- return nil, gtserror.NewErrorNotAuthorized(err)
+ return nil, gtserror.NewErrorUnauthorized(err)
}
// authorize the request:
@@ -53,7 +53,7 @@ func (p *processor) GetOutbox(ctx context.Context, requestedUsername string, pag
return nil, gtserror.NewErrorInternalError(err)
}
if blocked {
- return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
+ return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
}
var data map[string]interface{}
diff --git a/internal/processing/federation/getstatus.go b/internal/processing/federation/getstatus.go
index 2cc37071e..ef77d6c4f 100644
--- a/internal/processing/federation/getstatus.go
+++ b/internal/processing/federation/getstatus.go
@@ -42,7 +42,7 @@ func (p *processor) GetStatus(ctx context.Context, requestedUsername string, req
requestingAccount, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false, false)
if err != nil {
- return nil, gtserror.NewErrorNotAuthorized(err)
+ return nil, gtserror.NewErrorUnauthorized(err)
}
// authorize the request:
@@ -53,7 +53,7 @@ func (p *processor) GetStatus(ctx context.Context, requestedUsername string, req
}
if blocked {
- return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
+ return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
}
// get the status out of the database here
diff --git a/internal/processing/federation/getstatusreplies.go b/internal/processing/federation/getstatusreplies.go
index 984f3a407..3a2c9d944 100644
--- a/internal/processing/federation/getstatusreplies.go
+++ b/internal/processing/federation/getstatusreplies.go
@@ -44,7 +44,7 @@ func (p *processor) GetStatusReplies(ctx context.Context, requestedUsername stri
requestingAccount, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false, false)
if err != nil {
- return nil, gtserror.NewErrorNotAuthorized(err)
+ return nil, gtserror.NewErrorUnauthorized(err)
}
// authorize the request:
@@ -55,7 +55,7 @@ func (p *processor) GetStatusReplies(ctx context.Context, requestedUsername stri
}
if blocked {
- return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
+ return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
}
// get the status out of the database here
diff --git a/internal/processing/federation/getuser.go b/internal/processing/federation/getuser.go
index f870baa12..63c83f3c5 100644
--- a/internal/processing/federation/getuser.go
+++ b/internal/processing/federation/getuser.go
@@ -54,7 +54,7 @@ func (p *processor) GetUser(ctx context.Context, requestedUsername string, reque
if !p.federator.Handshaking(ctx, requestedUsername, requestingAccountURI) {
requestingAccount, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false, false)
if err != nil {
- return nil, gtserror.NewErrorNotAuthorized(err)
+ return nil, gtserror.NewErrorUnauthorized(err)
}
blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true)
@@ -63,7 +63,7 @@ func (p *processor) GetUser(ctx context.Context, requestedUsername string, reque
}
if blocked {
- return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
+ return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
}
}
diff --git a/internal/processing/media.go b/internal/processing/media.go
index 907ce1fd2..2522c4bfb 100644
--- a/internal/processing/media.go
+++ b/internal/processing/media.go
@@ -26,7 +26,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
-func (p *processor) MediaCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) {
+func (p *processor) MediaCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, gtserror.WithCode) {
return p.mediaProcessor.Create(ctx, authed.Account, form)
}
diff --git a/internal/processing/media/create.go b/internal/processing/media/create.go
index 1eb9fa512..1f40ac48f 100644
--- a/internal/processing/media/create.go
+++ b/internal/processing/media/create.go
@@ -24,11 +24,12 @@ import (
"io"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
)
-func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) {
+func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, gtserror.WithCode) {
data := func(innerCtx context.Context) (io.Reader, int, error) {
f, err := form.File.Open()
return f, int(form.File.Size), err
@@ -36,7 +37,8 @@ func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, form
focusX, focusY, err := parseFocus(form.Focus)
if err != nil {
- return nil, fmt.Errorf("could not parse focus value %s: %s", form.Focus, err)
+ err := fmt.Errorf("could not parse focus value %s: %s", form.Focus, err)
+ return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
// process the media attachment and load it immediately
@@ -46,19 +48,18 @@ func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, form
FocusY: &focusY,
})
if err != nil {
- return nil, err
+ return nil, gtserror.NewErrorUnprocessableEntity(err)
}
attachment, err := media.LoadAttachment(ctx)
if err != nil {
- return nil, err
+ return nil, gtserror.NewErrorUnprocessableEntity(err)
}
- // prepare the frontend representation now -- if there are any errors here at least we can bail without
- // having already put something in the database and then having to clean it up again (eugh)
apiAttachment, err := p.tc.AttachmentToAPIAttachment(ctx, attachment)
if err != nil {
- return nil, fmt.Errorf("error parsing media attachment to frontend type: %s", err)
+ err := fmt.Errorf("error parsing media attachment to frontend type: %s", err)
+ return nil, gtserror.NewErrorInternalError(err)
}
return &apiAttachment, nil
diff --git a/internal/processing/media/media.go b/internal/processing/media/media.go
index e895e0837..05bea615f 100644
--- a/internal/processing/media/media.go
+++ b/internal/processing/media/media.go
@@ -34,7 +34,7 @@ import (
// Processor wraps a bunch of functions for processing media actions.
type Processor interface {
// Create creates a new media attachment belonging to the given account, using the request form.
- Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error)
+ Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, gtserror.WithCode)
// Delete deletes the media attachment with the given ID, including all files pertaining to that attachment.
Delete(ctx context.Context, mediaAttachmentID string) gtserror.WithCode
// GetFile retrieves a file from storage and streams it back to the caller via an io.reader embedded in *apimodel.Content.
diff --git a/internal/processing/processor.go b/internal/processing/processor.go
index a10e0d6b3..a7a1c22e0 100644
--- a/internal/processing/processor.go
+++ b/internal/processing/processor.go
@@ -72,7 +72,7 @@ type Processor interface {
*/
// AccountCreate processes the given form for creating a new account, returning an oauth token for that account if successful.
- AccountCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountCreateRequest) (*apimodel.Token, error)
+ AccountCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountCreateRequest) (*apimodel.Token, gtserror.WithCode)
// AccountDeleteLocal processes the delete of a LOCAL account using the given form.
AccountDeleteLocal(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountDeleteRequest) gtserror.WithCode
// AccountGet processes the given request for account information.
@@ -80,7 +80,7 @@ type Processor interface {
// AccountGet processes the given request for account information.
AccountGetLocalByUsername(ctx context.Context, authed *oauth.Auth, username string) (*apimodel.Account, gtserror.WithCode)
// AccountUpdate processes the update of an account with the given form
- AccountUpdate(ctx context.Context, authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error)
+ AccountUpdate(ctx context.Context, authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode)
// AccountStatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for
// the account given in authed.
AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) (*apimodel.TimelineResponse, gtserror.WithCode)
@@ -117,7 +117,7 @@ type Processor interface {
AdminMediaPrune(ctx context.Context, mediaRemoteCacheDays int) gtserror.WithCode
// AppCreate processes the creation of a new API application
- AppCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, error)
+ AppCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, gtserror.WithCode)
// BlocksGet returns a list of accounts blocked by the requesting account.
BlocksGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, limit int) (*apimodel.BlocksResponse, gtserror.WithCode)
@@ -143,7 +143,7 @@ type Processor interface {
InstancePatch(ctx context.Context, form *apimodel.InstanceSettingsUpdateRequest) (*apimodel.Instance, gtserror.WithCode)
// MediaCreate handles the creation of a media attachment, using the given form.
- MediaCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error)
+ MediaCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, gtserror.WithCode)
// MediaGet handles the GET of a media attachment with the given ID
MediaGet(ctx context.Context, authed *oauth.Auth, attachmentID string) (*apimodel.Attachment, gtserror.WithCode)
// MediaUpdate handles the PUT of a media attachment with the given ID and form
@@ -156,11 +156,11 @@ type Processor interface {
SearchGet(ctx context.Context, authed *oauth.Auth, searchQuery *apimodel.SearchQuery) (*apimodel.SearchResult, gtserror.WithCode)
// StatusCreate processes the given form to create a new status, returning the api model representation of that status if it's OK.
- StatusCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, error)
+ StatusCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode)
// StatusDelete processes the delete of a given status, returning the deleted status if the delete goes through.
- StatusDelete(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error)
+ StatusDelete(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
// StatusFave processes the faving of a given status, returning the updated status if the fave goes through.
- StatusFave(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error)
+ StatusFave(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
// StatusBoost processes the boost/reblog of a given status, returning the newly-created boost if all is well.
StatusBoost(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
// StatusUnboost processes the unboost/unreblog of a given status, returning the status if all is well.
@@ -168,11 +168,11 @@ type Processor interface {
// StatusBoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings.
StatusBoostedBy(ctx context.Context, authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode)
// StatusFavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings.
- StatusFavedBy(ctx context.Context, authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, error)
+ StatusFavedBy(ctx context.Context, authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode)
// StatusGet gets the given status, taking account of privacy settings and blocks etc.
- StatusGet(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error)
+ StatusGet(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
// StatusUnfave processes the unfaving of a given status, returning the updated status if the fave goes through.
- StatusUnfave(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error)
+ StatusUnfave(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
// StatusGetContext returns the context (previous and following posts) from the given status ID
StatusGetContext(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Context, gtserror.WithCode)
@@ -184,7 +184,7 @@ type Processor interface {
FavedTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.TimelineResponse, gtserror.WithCode)
// AuthorizeStreamingRequest returns a gotosocial account in exchange for an access token, or an error if the given token is not valid.
- AuthorizeStreamingRequest(ctx context.Context, accessToken string) (*gtsmodel.Account, error)
+ AuthorizeStreamingRequest(ctx context.Context, accessToken string) (*gtsmodel.Account, gtserror.WithCode)
// OpenStreamForAccount opens a new stream for the given account, with the given stream type.
OpenStreamForAccount(ctx context.Context, account *gtsmodel.Account, streamType string) (*stream.Stream, gtserror.WithCode)
diff --git a/internal/processing/status.go b/internal/processing/status.go
index 5f287488a..b2f222971 100644
--- a/internal/processing/status.go
+++ b/internal/processing/status.go
@@ -26,15 +26,15 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
-func (p *processor) StatusCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, error) {
+func (p *processor) StatusCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode) {
return p.statusProcessor.Create(ctx, authed.Account, authed.Application, form)
}
-func (p *processor) StatusDelete(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) {
+func (p *processor) StatusDelete(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
return p.statusProcessor.Delete(ctx, authed.Account, targetStatusID)
}
-func (p *processor) StatusFave(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) {
+func (p *processor) StatusFave(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
return p.statusProcessor.Fave(ctx, authed.Account, targetStatusID)
}
@@ -50,15 +50,15 @@ func (p *processor) StatusBoostedBy(ctx context.Context, authed *oauth.Auth, tar
return p.statusProcessor.BoostedBy(ctx, authed.Account, targetStatusID)
}
-func (p *processor) StatusFavedBy(ctx context.Context, authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, error) {
+func (p *processor) StatusFavedBy(ctx context.Context, authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) {
return p.statusProcessor.FavedBy(ctx, authed.Account, targetStatusID)
}
-func (p *processor) StatusGet(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) {
+func (p *processor) StatusGet(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
return p.statusProcessor.Get(ctx, authed.Account, targetStatusID)
}
-func (p *processor) StatusUnfave(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) {
+func (p *processor) StatusUnfave(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
return p.statusProcessor.Unfave(ctx, authed.Account, targetStatusID)
}
diff --git a/internal/processing/status/create.go b/internal/processing/status/create.go
index e5f6e9647..2c509809a 100644
--- a/internal/processing/status/create.go
+++ b/internal/processing/status/create.go
@@ -57,8 +57,8 @@ func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, appli
Text: form.Status,
}
- if err := p.ProcessReplyToID(ctx, form, account.ID, newStatus); err != nil {
- return nil, gtserror.NewErrorInternalError(err)
+ if errWithCode := p.ProcessReplyToID(ctx, form, account.ID, newStatus); errWithCode != nil {
+ return nil, errWithCode
}
if err := p.ProcessMediaIDs(ctx, form, account.ID, newStatus); err != nil {
diff --git a/internal/processing/status/status.go b/internal/processing/status/status.go
index e8b4a8268..acd588461 100644
--- a/internal/processing/status/status.go
+++ b/internal/processing/status/status.go
@@ -60,7 +60,7 @@ type Processor interface {
*/
ProcessVisibility(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error
- ProcessReplyToID(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error
+ ProcessReplyToID(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode
ProcessMediaIDs(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error
ProcessLanguage(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountDefaultLanguage string, status *gtsmodel.Status) error
ProcessMentions(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error
diff --git a/internal/processing/status/util.go b/internal/processing/status/util.go
index df645189e..79c416f98 100644
--- a/internal/processing/status/util.go
+++ b/internal/processing/status/util.go
@@ -26,6 +26,7 @@ import (
"github.com/sirupsen/logrus"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
@@ -103,7 +104,7 @@ func (p *processor) ProcessVisibility(ctx context.Context, form *apimodel.Advanc
return nil
}
-func (p *processor) ProcessReplyToID(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error {
+func (p *processor) ProcessReplyToID(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode {
if form.InReplyToID == "" {
return nil
}
@@ -117,32 +118,37 @@ func (p *processor) ProcessReplyToID(ctx context.Context, form *apimodel.Advance
// If this is all OK, then we fetch the repliedStatus and the repliedAccount for later processing.
repliedStatus := >smodel.Status{}
repliedAccount := >smodel.Account{}
- // check replied status exists + is replyable
+
if err := p.db.GetByID(ctx, form.InReplyToID, repliedStatus); err != nil {
if err == db.ErrNoEntries {
- return fmt.Errorf("status with id %s not replyable because it doesn't exist", form.InReplyToID)
+ err := fmt.Errorf("status with id %s not replyable because it doesn't exist", form.InReplyToID)
+ return gtserror.NewErrorBadRequest(err, err.Error())
}
- return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
+ err := fmt.Errorf("db error fetching status with id %s: %s", form.InReplyToID, err)
+ return gtserror.NewErrorInternalError(err)
}
if !repliedStatus.Replyable {
- return fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID)
+ err := fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID)
+ return gtserror.NewErrorForbidden(err, err.Error())
}
- // check replied account is known to us
if err := p.db.GetByID(ctx, repliedStatus.AccountID, repliedAccount); err != nil {
if err == db.ErrNoEntries {
- return fmt.Errorf("status with id %s not replyable because account id %s is not known", form.InReplyToID, repliedStatus.AccountID)
+ err := fmt.Errorf("status with id %s not replyable because account id %s is not known", form.InReplyToID, repliedStatus.AccountID)
+ return gtserror.NewErrorBadRequest(err, err.Error())
}
- return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
+ err := fmt.Errorf("db error fetching account with id %s: %s", repliedStatus.AccountID, err)
+ return gtserror.NewErrorInternalError(err)
}
- // check if a block exists
+
if blocked, err := p.db.IsBlocked(ctx, thisAccountID, repliedAccount.ID, true); err != nil {
- if err != db.ErrNoEntries {
- return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
- }
+ err := fmt.Errorf("db error checking block: %s", err)
+ return gtserror.NewErrorInternalError(err)
} else if blocked {
- return fmt.Errorf("status with id %s not replyable", form.InReplyToID)
+ err := fmt.Errorf("status with id %s not replyable", form.InReplyToID)
+ return gtserror.NewErrorNotFound(err)
}
+
status.InReplyToID = repliedStatus.ID
status.InReplyToAccountID = repliedAccount.ID
diff --git a/internal/processing/streaming.go b/internal/processing/streaming.go
index 0d51629fa..c5ab52d6e 100644
--- a/internal/processing/streaming.go
+++ b/internal/processing/streaming.go
@@ -26,7 +26,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/stream"
)
-func (p *processor) AuthorizeStreamingRequest(ctx context.Context, accessToken string) (*gtsmodel.Account, error) {
+func (p *processor) AuthorizeStreamingRequest(ctx context.Context, accessToken string) (*gtsmodel.Account, gtserror.WithCode) {
return p.streamingProcessor.AuthorizeStreamingRequest(ctx, accessToken)
}
diff --git a/internal/processing/streaming/authorize.go b/internal/processing/streaming/authorize.go
index 9f014e723..70e4741e1 100644
--- a/internal/processing/streaming/authorize.go
+++ b/internal/processing/streaming/authorize.go
@@ -22,29 +22,40 @@ import (
"context"
"fmt"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
-func (p *processor) AuthorizeStreamingRequest(ctx context.Context, accessToken string) (*gtsmodel.Account, error) {
+func (p *processor) AuthorizeStreamingRequest(ctx context.Context, accessToken string) (*gtsmodel.Account, gtserror.WithCode) {
ti, err := p.oauthServer.LoadAccessToken(ctx, accessToken)
if err != nil {
- return nil, fmt.Errorf("AuthorizeStreamingRequest: error loading access token: %s", err)
+ err := fmt.Errorf("could not load access token: %s", err)
+ return nil, gtserror.NewErrorUnauthorized(err)
}
uid := ti.GetUserID()
if uid == "" {
- return nil, fmt.Errorf("AuthorizeStreamingRequest: no userid in token")
+ err := fmt.Errorf("no userid in token")
+ return nil, gtserror.NewErrorUnauthorized(err)
}
- // fetch user's and account for this user id
user := >smodel.User{}
- if err := p.db.GetByID(ctx, uid, user); err != nil || user == nil {
- return nil, fmt.Errorf("AuthorizeStreamingRequest: no user found for validated uid %s", uid)
+ if err := p.db.GetByID(ctx, uid, user); err != nil {
+ if err == db.ErrNoEntries {
+ err := fmt.Errorf("no user found for validated uid %s", uid)
+ return nil, gtserror.NewErrorUnauthorized(err)
+ }
+ return nil, gtserror.NewErrorInternalError(err)
}
acct, err := p.db.GetAccountByID(ctx, user.AccountID)
- if err != nil || acct == nil {
- return nil, fmt.Errorf("AuthorizeStreamingRequest: no account retrieved for user with id %s", uid)
+ if err != nil {
+ if err == db.ErrNoEntries {
+ err := fmt.Errorf("no account found for validated uid %s", uid)
+ return nil, gtserror.NewErrorUnauthorized(err)
+ }
+ return nil, gtserror.NewErrorInternalError(err)
}
return acct, nil
diff --git a/internal/processing/streaming/authorize_test.go b/internal/processing/streaming/authorize_test.go
index b6b1db729..51cec38be 100644
--- a/internal/processing/streaming/authorize_test.go
+++ b/internal/processing/streaming/authorize_test.go
@@ -39,7 +39,7 @@ func (suite *AuthorizeTestSuite) TestAuthorize() {
suite.Equal(suite.testAccounts["local_account_2"].ID, account2.ID)
noAccount, err := suite.streamingProcessor.AuthorizeStreamingRequest(context.Background(), "aaaaaaaaaaaaaaaaaaaaa!!")
- suite.EqualError(err, "AuthorizeStreamingRequest: error loading access token: no entries")
+ suite.EqualError(err, "could not load access token: no entries")
suite.Nil(noAccount)
}
diff --git a/internal/processing/streaming/streaming.go b/internal/processing/streaming/streaming.go
index defe52a9c..88c8dde05 100644
--- a/internal/processing/streaming/streaming.go
+++ b/internal/processing/streaming/streaming.go
@@ -33,7 +33,7 @@ import (
// Processor wraps a bunch of functions for processing streaming.
type Processor interface {
// AuthorizeStreamingRequest returns an oauth2 token info in response to an access token query from the streaming API
- AuthorizeStreamingRequest(ctx context.Context, accessToken string) (*gtsmodel.Account, error)
+ AuthorizeStreamingRequest(ctx context.Context, accessToken string) (*gtsmodel.Account, gtserror.WithCode)
// OpenStreamForAccount returns a new Stream for the given account, which will contain a channel for passing messages back to the caller.
OpenStreamForAccount(ctx context.Context, account *gtsmodel.Account, timeline string) (*stream.Stream, gtserror.WithCode)
// StreamUpdateToAccount streams the given update to any open, appropriate streams belonging to the given account.
diff --git a/internal/processing/user/changepassword_test.go b/internal/processing/user/changepassword_test.go
index b88b11b3d..e769f4cc0 100644
--- a/internal/processing/user/changepassword_test.go
+++ b/internal/processing/user/changepassword_test.go
@@ -57,7 +57,7 @@ func (suite *ChangePasswordTestSuite) TestChangePasswordIncorrectOld() {
errWithCode := suite.user.ChangePassword(context.Background(), user, "ooooopsydoooopsy", "verygoodnewpassword")
suite.EqualError(errWithCode, "crypto/bcrypt: hashedPassword is not the hash of the given password")
suite.Equal(http.StatusBadRequest, errWithCode.Code())
- suite.Equal("bad request: old password did not match", errWithCode.Safe())
+ suite.Equal("Bad Request: old password did not match", errWithCode.Safe())
}
func (suite *ChangePasswordTestSuite) TestChangePasswordWeakNew() {
@@ -66,7 +66,7 @@ func (suite *ChangePasswordTestSuite) TestChangePasswordWeakNew() {
errWithCode := suite.user.ChangePassword(context.Background(), user, "password", "1234")
suite.EqualError(errWithCode, "password is 11% strength, try including more special characters, using lowercase letters, using uppercase letters or using a longer password")
suite.Equal(http.StatusBadRequest, errWithCode.Code())
- suite.Equal("bad request: password is 11% strength, try including more special characters, using lowercase letters, using uppercase letters or using a longer password", errWithCode.Safe())
+ suite.Equal("Bad Request: password is 11% strength, try including more special characters, using lowercase letters, using uppercase letters or using a longer password", errWithCode.Safe())
}
func TestChangePasswordTestSuite(t *testing.T) {
diff --git a/internal/transport/deliver.go b/internal/transport/deliver.go
index 0405116ad..16ebbd4b6 100644
--- a/internal/transport/deliver.go
+++ b/internal/transport/deliver.go
@@ -27,6 +27,7 @@ import (
"strings"
"sync"
+ "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/config"
)
@@ -79,7 +80,7 @@ func (t *transport) Deliver(ctx context.Context, b []byte, to *url.URL) error {
return err
}
- req.Header.Add("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")
+ req.Header.Add("Content-Type", string(api.AppActivityLDJSON))
req.Header.Add("Accept-Charset", "utf-8")
req.Header.Add("User-Agent", t.controller.userAgent)
req.Header.Set("Host", to.Host)
diff --git a/internal/transport/dereference.go b/internal/transport/dereference.go
index ba48488d7..cb1220d13 100644
--- a/internal/transport/dereference.go
+++ b/internal/transport/dereference.go
@@ -25,6 +25,7 @@ import (
"net/http"
"net/url"
+ "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/uris"
)
@@ -52,7 +53,8 @@ func (t *transport) Dereference(ctx context.Context, iri *url.URL) ([]byte, erro
if err != nil {
return nil, err
}
- req.Header.Add("Accept", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\",application/activity+json")
+
+ req.Header.Add("Accept", string(api.AppActivityLDJSON)+","+string(api.AppActivityJSON))
req.Header.Add("Accept-Charset", "utf-8")
req.Header.Add("User-Agent", t.controller.userAgent)
req.Header.Set("Host", iri.Host)
diff --git a/internal/transport/derefinstance.go b/internal/transport/derefinstance.go
index 1acbcc364..b99d28a23 100644
--- a/internal/transport/derefinstance.go
+++ b/internal/transport/derefinstance.go
@@ -30,6 +30,7 @@ import (
"github.com/sirupsen/logrus"
+ "github.com/superseriousbusiness/gotosocial/internal/api"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
@@ -94,7 +95,7 @@ func dereferenceByAPIV1Instance(ctx context.Context, t *transport, iri *url.URL)
return nil, err
}
- req.Header.Add("Accept", "application/json")
+ req.Header.Add("Accept", string(api.AppJSON))
req.Header.Add("User-Agent", t.controller.userAgent)
req.Header.Set("Host", cleanIRI.Host)
@@ -245,7 +246,7 @@ func callNodeInfoWellKnown(ctx context.Context, t *transport, iri *url.URL) (*ur
if err != nil {
return nil, err
}
- req.Header.Add("Accept", "application/json")
+ req.Header.Add("Accept", string(api.AppJSON))
req.Header.Add("User-Agent", t.controller.userAgent)
req.Header.Set("Host", cleanIRI.Host)
@@ -297,7 +298,7 @@ func callNodeInfo(ctx context.Context, t *transport, iri *url.URL) (*apimodel.No
if err != nil {
return nil, err
}
- req.Header.Add("Accept", "application/json")
+ req.Header.Add("Accept", string(api.AppJSON))
req.Header.Add("User-Agent", t.controller.userAgent)
req.Header.Set("Host", iri.Host)
diff --git a/internal/transport/finger.go b/internal/transport/finger.go
index 7554a242f..2ea34ad81 100644
--- a/internal/transport/finger.go
+++ b/internal/transport/finger.go
@@ -23,6 +23,8 @@ import (
"fmt"
"io/ioutil"
"net/http"
+
+ "github.com/superseriousbusiness/gotosocial/internal/api"
)
func (t *transport) Finger(ctx context.Context, targetUsername string, targetDomain string) ([]byte, error) {
@@ -37,7 +39,7 @@ func (t *transport) Finger(ctx context.Context, targetUsername string, targetDom
if err != nil {
return nil, err
}
- req.Header.Add("Accept", "application/json")
+ req.Header.Add("Accept", string(api.AppJSON))
req.Header.Add("Accept", "application/jrd+json")
req.Header.Add("User-Agent", t.controller.userAgent)
req.Header.Set("Host", req.URL.Host)
diff --git a/internal/web/base.go b/internal/web/base.go
index d203522ae..a8d99619c 100644
--- a/internal/web/base.go
+++ b/internal/web/base.go
@@ -19,6 +19,7 @@
package web
import (
+ "errors"
"fmt"
"io/ioutil"
"net/http"
@@ -29,6 +30,7 @@ import (
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/processing"
"github.com/superseriousbusiness/gotosocial/internal/router"
"github.com/superseriousbusiness/gotosocial/internal/uris"
@@ -118,24 +120,6 @@ func (m *Module) baseHandler(c *gin.Context) {
})
}
-// NotFoundHandler serves a 404 html page instead of a blank 404 error.
-func (m *Module) NotFoundHandler(c *gin.Context) {
- l := logrus.WithField("func", "404")
- l.Trace("serving 404 html")
-
- host := config.GetHost()
- instance, err := m.processor.InstanceGet(c.Request.Context(), host)
- if err != nil {
- l.Debugf("error getting instance from processor: %s", err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
- return
- }
-
- c.HTML(404, "404.tmpl", gin.H{
- "instance": instance,
- })
-}
-
// Route satisfies the RESTAPIModule interface
func (m *Module) Route(s router.Router) error {
// serve static files from assets dir at /assets
@@ -152,16 +136,18 @@ func (m *Module) Route(s router.Router) error {
s.AttachHandler(http.MethodGet, "/", m.baseHandler)
// serve profile pages at /@username
- s.AttachHandler(http.MethodGet, profilePath, m.profileTemplateHandler)
+ s.AttachHandler(http.MethodGet, profilePath, m.profileGETHandler)
// serve statuses
- s.AttachHandler(http.MethodGet, statusPath, m.threadTemplateHandler)
+ s.AttachHandler(http.MethodGet, statusPath, m.threadGETHandler)
// serve email confirmation page at /confirm_email?token=whatever
s.AttachHandler(http.MethodGet, confirmEmailPath, m.confirmEmailGETHandler)
// 404 handler
- s.AttachNoRouteHandler(m.NotFoundHandler)
+ s.AttachNoRouteHandler(func(c *gin.Context) {
+ api.ErrorHandler(c, gtserror.NewErrorNotFound(errors.New(http.StatusText(http.StatusNotFound))), m.processor.InstanceGet)
+ })
return nil
}
diff --git a/internal/web/confirmemail.go b/internal/web/confirmemail.go
index 7c8aa464f..58f932bde 100644
--- a/internal/web/confirmemail.go
+++ b/internal/web/confirmemail.go
@@ -19,35 +19,35 @@
package web
import (
+ "errors"
"net/http"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
+ "github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
func (m *Module) confirmEmailGETHandler(c *gin.Context) {
+ ctx := c.Request.Context()
+
// if there's no token in the query, just serve the 404 web handler
token := c.Query(tokenParam)
if token == "" {
- m.NotFoundHandler(c)
+ api.ErrorHandler(c, gtserror.NewErrorNotFound(errors.New(http.StatusText(http.StatusNotFound))), m.processor.InstanceGet)
return
}
- ctx := c.Request.Context()
-
user, errWithCode := m.processor.UserConfirmEmail(ctx, token)
if errWithCode != nil {
- logrus.Debugf("error confirming email: %s", errWithCode.Error())
- // if something goes wrong, just log it and direct to the 404 handler to not give anything away
- m.NotFoundHandler(c)
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
host := config.GetHost()
instance, err := m.processor.InstanceGet(ctx, host)
if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
return
}
diff --git a/internal/web/profile.go b/internal/web/profile.go
index 3155c022d..051d55d28 100644
--- a/internal/web/profile.go
+++ b/internal/web/profile.go
@@ -21,59 +21,60 @@ package web
import (
"context"
"encoding/json"
+ "errors"
"fmt"
"math/rand"
"net/http"
+ "strings"
"github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/api"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
-func (m *Module) profileTemplateHandler(c *gin.Context) {
- l := logrus.WithField("func", "profileTemplateHandler")
- l.Trace("rendering profile template")
+func (m *Module) profileGETHandler(c *gin.Context) {
ctx := c.Request.Context()
- username := c.Param(usernameKey)
- if username == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no account username specified"})
- return
- }
-
authed, err := oauth.Authed(c, false, false, false, false)
if err != nil {
- l.Errorf("error authing profile GET request: %s", err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}
- instance, errWithCode := m.processor.InstanceGet(ctx, config.GetHost())
- if errWithCode != nil {
- l.Debugf("error getting instance from processor: %s", errWithCode.Error())
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ // usernames on our instance will always be lowercase
+ username := strings.ToLower(c.Param(usernameKey))
+ if username == "" {
+ err := errors.New("no account username specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
+ host := config.GetHost()
+ instance, err := m.processor.InstanceGet(ctx, host)
+ if err != nil {
+ api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
+ return
+ }
+
+ instanceGet := func(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode) {
+ return instance, nil
+ }
+
account, errWithCode := m.processor.AccountGetLocalByUsername(ctx, authed, username)
if errWithCode != nil {
- l.Debugf("error getting account from processor: %s", errWithCode.Error())
- if errWithCode.Code() == http.StatusNotFound {
- m.NotFoundHandler(c)
- return
- }
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, instanceGet)
return
}
- // if we're getting an AP request on this endpoint we should render the account's AP representation instead
+ // if we're getting an AP request on this endpoint we
+ // should render the account's AP representation instead
accept := c.NegotiateFormat(string(api.TextHTML), string(api.AppActivityJSON), string(api.AppActivityLDJSON))
if accept == string(api.AppActivityJSON) || accept == string(api.AppActivityLDJSON) {
- m.returnAPRepresentation(ctx, c, username, accept)
+ m.returnAPProfile(ctx, c, username, accept)
return
}
@@ -82,8 +83,7 @@ func (m *Module) profileTemplateHandler(c *gin.Context) {
// with or without media
statusResp, errWithCode := m.processor.AccountStatusesGet(ctx, authed, account.ID, 10, true, true, "", "", false, false, true)
if errWithCode != nil {
- l.Debugf("error getting statuses from processor: %s", errWithCode.Error())
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, instanceGet)
return
}
@@ -114,7 +114,7 @@ func (m *Module) profileTemplateHandler(c *gin.Context) {
})
}
-func (m *Module) returnAPRepresentation(ctx context.Context, c *gin.Context, username string, accept string) {
+func (m *Module) returnAPProfile(ctx context.Context, c *gin.Context, username string, accept string) {
verifier, signed := c.Get(string(ap.ContextRequestingPublicKeyVerifier))
if signed {
ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeyVerifier, verifier)
@@ -125,17 +125,16 @@ func (m *Module) returnAPRepresentation(ctx context.Context, c *gin.Context, use
ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeySignature, signature)
}
- user, errWithCode := m.processor.GetFediUser(ctx, username, c.Request.URL) // GetFediUser handles auth as well
+ user, errWithCode := m.processor.GetFediUser(ctx, username, c.Request.URL)
if errWithCode != nil {
- logrus.Infof(errWithCode.Error())
- c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}
b, mErr := json.Marshal(user)
if mErr != nil {
err := fmt.Errorf("could not marshal json: %s", mErr)
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
return
}
diff --git a/internal/web/thread.go b/internal/web/thread.go
index 3a9839281..5e953ee06 100644
--- a/internal/web/thread.go
+++ b/internal/web/thread.go
@@ -19,65 +19,88 @@
package web
import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
"net/http"
"strings"
- "github.com/sirupsen/logrus"
-
"github.com/gin-gonic/gin"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
+ "github.com/superseriousbusiness/gotosocial/internal/api"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
-func (m *Module) threadTemplateHandler(c *gin.Context) {
- l := logrus.WithField("func", "threadTemplateGET")
- l.Trace("rendering thread template")
-
+func (m *Module) threadGETHandler(c *gin.Context) {
ctx := c.Request.Context()
+ authed, err := oauth.Authed(c, false, false, false, false)
+ if err != nil {
+ api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
+ return
+ }
+
// usernames on our instance will always be lowercase
username := strings.ToLower(c.Param(usernameKey))
if username == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no account username specified"})
+ err := errors.New("no account username specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
// status ids will always be uppercase
statusID := strings.ToUpper(c.Param(statusIDKey))
if statusID == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "no status id specified"})
- return
- }
-
- authed, err := oauth.Authed(c, false, false, false, false)
- if err != nil {
- l.Errorf("error authing status GET request: %s", err)
- c.JSON(http.StatusBadRequest, gin.H{"error": "status not found"})
+ err := errors.New("no status id specified")
+ api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}
host := config.GetHost()
instance, err := m.processor.InstanceGet(ctx, host)
if err != nil {
- l.Debugf("error getting instance from processor: %s", err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
+ api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
return
}
- status, err := m.processor.StatusGet(ctx, authed, statusID)
- if err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": "status not found"})
+ instanceGet := func(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode) {
+ return instance, nil
+ }
+
+ // do this check to make sure the status is actually from a local account,
+ // we shouldn't render threads from statuses that don't belong to us!
+ if _, errWithCode := m.processor.AccountGetLocalByUsername(ctx, authed, username); errWithCode != nil {
+ api.ErrorHandler(c, errWithCode, instanceGet)
+ return
+ }
+
+ status, errWithCode := m.processor.StatusGet(ctx, authed, statusID)
+ if errWithCode != nil {
+ api.ErrorHandler(c, errWithCode, instanceGet)
return
}
if !strings.EqualFold(username, status.Account.Username) {
- c.JSON(http.StatusBadRequest, gin.H{"error": "status not found"})
+ err := gtserror.NewErrorNotFound(errors.New("path username not equal to status author username"))
+ api.ErrorHandler(c, gtserror.NewErrorNotFound(err), instanceGet)
return
}
- context, err := m.processor.StatusGetContext(ctx, authed, statusID)
- if err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": "status not found"})
+ // if we're getting an AP request on this endpoint we
+ // should render the status's AP representation instead
+ accept := c.NegotiateFormat(string(api.TextHTML), string(api.AppActivityJSON), string(api.AppActivityLDJSON))
+ if accept == string(api.AppActivityJSON) || accept == string(api.AppActivityLDJSON) {
+ m.returnAPStatus(ctx, c, username, statusID, accept)
+ return
+ }
+
+ context, errWithCode := m.processor.StatusGetContext(ctx, authed, statusID)
+ if errWithCode != nil {
+ api.ErrorHandler(c, errWithCode, instanceGet)
return
}
@@ -88,3 +111,30 @@ func (m *Module) threadTemplateHandler(c *gin.Context) {
"stylesheets": []string{"/assets/Fork-Awesome/css/fork-awesome.min.css", "/assets/status.css"},
})
}
+
+func (m *Module) returnAPStatus(ctx context.Context, c *gin.Context, username string, statusID string, accept string) {
+ verifier, signed := c.Get(string(ap.ContextRequestingPublicKeyVerifier))
+ if signed {
+ ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeyVerifier, verifier)
+ }
+
+ signature, signed := c.Get(string(ap.ContextRequestingPublicKeySignature))
+ if signed {
+ ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeySignature, signature)
+ }
+
+ status, errWithCode := m.processor.GetFediStatus(ctx, username, statusID, c.Request.URL)
+ if errWithCode != nil {
+ api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
+ return
+ }
+
+ b, mErr := json.Marshal(status)
+ if mErr != nil {
+ err := fmt.Errorf("could not marshal json: %s", mErr)
+ api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
+ return
+ }
+
+ c.Data(http.StatusOK, accept, b)
+}
diff --git a/testrig/router.go b/testrig/router.go
index e06e21bd4..5ab228611 100644
--- a/testrig/router.go
+++ b/testrig/router.go
@@ -20,10 +20,8 @@ package testrig
import (
"context"
- "fmt"
"os"
"path/filepath"
- "runtime"
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/config"
@@ -54,25 +52,7 @@ func NewTestRouter(db db.DB) router.Router {
}
// ConfigureTemplatesWithGin will panic on any errors related to template loading during tests
-func ConfigureTemplatesWithGin(engine *gin.Engine) {
+func ConfigureTemplatesWithGin(engine *gin.Engine, templatePath string) {
router.LoadTemplateFunctions(engine)
-
- templateBaseDir := config.GetWebTemplateBaseDir()
-
- if !filepath.IsAbs(templateBaseDir) {
- // https://stackoverflow.com/questions/31873396/is-it-possible-to-get-the-current-root-of-package-structure-as-a-string-in-golan
- _, runtimeCallerLocation, _, _ := runtime.Caller(0)
- projectRoot, err := filepath.Abs(filepath.Join(filepath.Dir(runtimeCallerLocation), "../"))
- if err != nil {
- panic(err)
- }
-
- templateBaseDir = filepath.Join(projectRoot, templateBaseDir)
- }
-
- if _, err := os.Stat(filepath.Join(templateBaseDir, "index.tmpl")); err != nil {
- panic(fmt.Errorf("%s doesn't seem to contain the templates; index.tmpl is missing: %w", templateBaseDir, err))
- }
-
- engine.LoadHTMLGlob(filepath.Join(templateBaseDir, "*"))
+ engine.LoadHTMLGlob(filepath.Join(templatePath, "*"))
}
diff --git a/web/template/404.tmpl b/web/template/404.tmpl
index 096f7f364..7153bfd40 100644
--- a/web/template/404.tmpl
+++ b/web/template/404.tmpl
@@ -2,7 +2,17 @@
404: Page Not Found
- If you believe this was an error, you can contact an admin
+
+ GoToSocial only serves Public statuses via the web.
+ If you reached this page by clicking on a status link,
+ it's possible that the status is not Public, has been
+ deleted by the author, you don't have permission to see
+ it, or it just doesn't exist at all.
+
+
+ If you believe this 404 was an error, you can contact
+ the instance admin.
+
diff --git a/web/template/error.tmpl b/web/template/error.tmpl
index a8a72b062..c7df695dd 100644
--- a/web/template/error.tmpl
+++ b/web/template/error.tmpl
@@ -3,6 +3,5 @@
-
{{ template "footer.tmpl" .}}
\ No newline at end of file
diff --git a/web/template/footer.tmpl b/web/template/footer.tmpl
index 5aba0e1a9..f95d8b71b 100644
--- a/web/template/footer.tmpl
+++ b/web/template/footer.tmpl
@@ -5,10 +5,14 @@
Source Code