diff options
| -rw-r--r-- | docs/api/swagger.yaml | 4 | ||||
| -rw-r--r-- | internal/api/client/statuses/statuscreate.go | 124 | ||||
| -rw-r--r-- | internal/api/client/statuses/statuscreate_test.go | 19 | ||||
| -rw-r--r-- | internal/gtserror/withcode.go | 13 | 
4 files changed, 113 insertions, 47 deletions
diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml index c0dc6de89..9f0e91cd7 100644 --- a/docs/api/swagger.yaml +++ b/docs/api/swagger.yaml @@ -8947,7 +8947,7 @@ paths:                      Providing this parameter will cause ScheduledStatus to be returned instead of Status.                      Must be at least 5 minutes in the future. -                    This feature isn't implemented yet. +                    This feature isn't implemented yet; attemping to set it will return 501 Not Implemented.                    in: formData                    name: scheduled_at                    type: string @@ -9008,6 +9008,8 @@ paths:                      description: not acceptable                  "500":                      description: internal server error +                "501": +                    description: scheduled_at was set, but this feature is not yet implemented              security:                  - OAuth2 Bearer:                      - write:statuses diff --git a/internal/api/client/statuses/statuscreate.go b/internal/api/client/statuses/statuscreate.go index 996f7605b..48d11f363 100644 --- a/internal/api/client/statuses/statuscreate.go +++ b/internal/api/client/statuses/statuscreate.go @@ -181,7 +181,7 @@ import (  //			Providing this parameter will cause ScheduledStatus to be returned instead of Status.  //			Must be at least 5 minutes in the future.  // -//			This feature isn't implemented yet. +//			This feature isn't implemented yet; attemping to set it will return 501 Not Implemented.  //		type: string  //		in: formData  //	- @@ -254,6 +254,8 @@ import (  //			description: not acceptable  //		'500':  //			description: internal server error +//		'501': +//			description: scheduled_at was set, but this feature is not yet implemented  func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {  	authed, err := oauth.Authed(c, true, true, true, true)  	if err != nil { @@ -286,8 +288,8 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {  	// }  	// form.Status += "\n\nsent from " + user + "'s iphone\n" -	if err := validateNormalizeCreateStatus(form); err != nil { -		apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) +	if errWithCode := validateStatusCreateForm(form); errWithCode != nil { +		apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)  		return  	} @@ -374,46 +376,61 @@ func parseStatusCreateForm(c *gin.Context) (*apimodel.StatusCreateRequest, error  	return form, nil  } -// validateNormalizeCreateStatus checks the form -// for disallowed combinations of attachments and -// overlength inputs. +// validateStatusCreateForm checks the form for disallowed +// combinations of attachments, overlength inputs, etc.  //  // Side effect: normalizes the post's language tag. -func validateNormalizeCreateStatus(form *apimodel.StatusCreateRequest) error { -	hasStatus := form.Status != "" -	hasMedia := len(form.MediaIDs) != 0 -	hasPoll := form.Poll != nil - -	if !hasStatus && !hasMedia && !hasPoll { -		return errors.New("no status, media, or poll provided") -	} +func validateStatusCreateForm(form *apimodel.StatusCreateRequest) gtserror.WithCode { +	var ( +		chars         = len([]rune(form.Status)) + len([]rune(form.SpoilerText)) +		maxChars      = config.GetStatusesMaxChars() +		mediaFiles    = len(form.MediaIDs) +		maxMediaFiles = config.GetStatusesMediaMaxFiles() +		hasMedia      = mediaFiles != 0 +		hasPoll       = form.Poll != nil +	) -	if hasMedia && hasPoll { -		return errors.New("can't post media + poll in same status") +	if chars == 0 && !hasMedia && !hasPoll { +		// Status must contain *some* kind of content. +		const text = "no status content, content warning, media, or poll provided" +		return gtserror.NewErrorBadRequest(errors.New(text), text)  	} -	maxChars := config.GetStatusesMaxChars() -	if length := len([]rune(form.Status)) + len([]rune(form.SpoilerText)); length > maxChars { -		return fmt.Errorf("status too long, %d characters provided (including spoiler/content warning) but limit is %d", length, maxChars) +	if chars > maxChars { +		text := fmt.Sprintf( +			"status too long, %d characters provided (including content warning) but limit is %d", +			chars, maxChars, +		) +		return gtserror.NewErrorBadRequest(errors.New(text), text)  	} -	maxMediaFiles := config.GetStatusesMediaMaxFiles() -	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) +	if mediaFiles > maxMediaFiles { +		text := fmt.Sprintf( +			"too many media files attached to status, %d attached but limit is %d", +			mediaFiles, maxMediaFiles, +		) +		return gtserror.NewErrorBadRequest(errors.New(text), text)  	}  	if form.Poll != nil { -		if err := validateNormalizeCreatePoll(form); err != nil { -			return err +		if errWithCode := validateStatusPoll(form); errWithCode != nil { +			return errWithCode  		}  	} +	if form.ScheduledAt != "" { +		const text = "scheduled_at is not yet implemented" +		return gtserror.NewErrorNotImplemented(errors.New(text), text) +	} + +	// Validate + normalize +	// language tag if provided.  	if form.Language != "" { -		language, err := validate.Language(form.Language) +		lang, err := validate.Language(form.Language)  		if err != nil { -			return err +			return gtserror.NewErrorBadRequest(err, err.Error())  		} -		form.Language = language +		form.Language = lang  	}  	// Check if the deprecated "federated" field was @@ -425,9 +442,36 @@ func validateNormalizeCreateStatus(form *apimodel.StatusCreateRequest) error {  	return nil  } -func validateNormalizeCreatePoll(form *apimodel.StatusCreateRequest) error { -	maxPollOptions := config.GetStatusesPollMaxOptions() -	maxPollChars := config.GetStatusesPollOptionMaxChars() +func validateStatusPoll(form *apimodel.StatusCreateRequest) gtserror.WithCode { +	var ( +		maxPollOptions     = config.GetStatusesPollMaxOptions() +		pollOptions        = len(form.Poll.Options) +		maxPollOptionChars = config.GetStatusesPollOptionMaxChars() +	) + +	if pollOptions == 0 { +		const text = "poll with no options" +		return gtserror.NewErrorBadRequest(errors.New(text), text) +	} + +	if pollOptions > maxPollOptions { +		text := fmt.Sprintf( +			"too many poll options provided, %d provided but limit is %d", +			pollOptions, maxPollOptions, +		) +		return gtserror.NewErrorBadRequest(errors.New(text), text) +	} + +	for _, option := range form.Poll.Options { +		optionChars := len([]rune(option)) +		if optionChars > maxPollOptionChars { +			text := fmt.Sprintf( +				"poll option too long, %d characters provided but limit is %d", +				optionChars, maxPollOptionChars, +			) +			return gtserror.NewErrorBadRequest(errors.New(text), text) +		} +	}  	// Normalize poll expiry if necessary.  	// If we parsed this as JSON, expires_in @@ -440,27 +484,15 @@ func validateNormalizeCreatePoll(form *apimodel.StatusCreateRequest) error {  		case string:  			expiresIn, err := strconv.Atoi(e)  			if err != nil { -				return fmt.Errorf("could not parse expires_in value %s as integer: %w", e, err) +				text := fmt.Sprintf("could not parse expires_in value %s as integer: %v", e, err) +				return gtserror.NewErrorBadRequest(errors.New(text), text)  			}  			form.Poll.ExpiresIn = expiresIn  		default: -			return fmt.Errorf("could not parse expires_in type %T as integer", ei) -		} -	} - -	if len(form.Poll.Options) == 0 { -		return errors.New("poll with no options") -	} - -	if len(form.Poll.Options) > maxPollOptions { -		return fmt.Errorf("too many poll options provided, %d provided but limit is %d", len(form.Poll.Options), maxPollOptions) -	} - -	for _, p := range form.Poll.Options { -		if length := len([]rune(p)); length > maxPollChars { -			return fmt.Errorf("poll option too long, %d characters provided but limit is %d", length, maxPollChars) +			text := fmt.Sprintf("could not parse expires_in type %T as integer", ei) +			return gtserror.NewErrorBadRequest(errors.New(text), text)  		}  	} diff --git a/internal/api/client/statuses/statuscreate_test.go b/internal/api/client/statuses/statuscreate_test.go index 8598b5ef0..5f5386dd5 100644 --- a/internal/api/client/statuses/statuscreate_test.go +++ b/internal/api/client/statuses/statuscreate_test.go @@ -365,6 +365,25 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusMessedUpIntPolicy() {  }`, out)  } +func (suite *StatusCreateTestSuite) TestPostNewScheduledStatus() { +	out, recorder := suite.postStatus(map[string][]string{ +		"status":       {"this is a brand new status! #helloworld"}, +		"spoiler_text": {"hello hello"}, +		"sensitive":    {"true"}, +		"visibility":   {string(apimodel.VisibilityMutualsOnly)}, +		"scheduled_at": {"2080-10-04T15:32:02.018Z"}, +	}, "") + +	// We should have 501 from +	// our call to the function. +	suite.Equal(http.StatusNotImplemented, recorder.Code) + +	// We should have a helpful error message. +	suite.Equal(`{ +  "error": "Not Implemented: scheduled_at is not yet implemented" +}`, out) +} +  func (suite *StatusCreateTestSuite) TestPostNewStatusMarkdown() {  	out, recorder := suite.postStatus(map[string][]string{  		"status":       {statusMarkdown}, diff --git a/internal/gtserror/withcode.go b/internal/gtserror/withcode.go index da489225c..0878db7bc 100644 --- a/internal/gtserror/withcode.go +++ b/internal/gtserror/withcode.go @@ -191,6 +191,19 @@ func NewErrorGone(original error, helpText ...string) WithCode {  	}  } +// NewErrorNotImplemented returns an ErrorWithCode 501 with the given original error and optional help text. +func NewErrorNotImplemented(original error, helpText ...string) WithCode { +	safe := http.StatusText(http.StatusNotImplemented) +	if helpText != nil { +		safe = safe + ": " + strings.Join(helpText, ": ") +	} +	return withCode{ +		original: original, +		safe:     errors.New(safe), +		code:     http.StatusNotImplemented, +	} +} +  // NewErrorClientClosedRequest returns an ErrorWithCode 499 with the given original error.  // This error type should only be used when an http caller has already hung up their request.  // See: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#nginx  | 
