diff options
| -rw-r--r-- | docs/api/swagger.yaml | 12 | ||||
| -rw-r--r-- | internal/api/client/account/accountupdate.go | 9 | ||||
| -rw-r--r-- | internal/api/client/account/accountupdate_test.go | 75 | ||||
| -rw-r--r-- | internal/api/client/status/statuscreate_test.go | 55 | ||||
| -rw-r--r-- | internal/api/model/account.go | 2 | ||||
| -rw-r--r-- | internal/api/model/source.go | 2 | ||||
| -rw-r--r-- | internal/api/model/status.go | 16 | ||||
| -rw-r--r-- | internal/cache/account.go | 1 | ||||
| -rw-r--r-- | internal/db/bundb/migrations/20220804120132_account_default_post_format.go | 46 | ||||
| -rw-r--r-- | internal/gtsmodel/account.go | 1 | ||||
| -rw-r--r-- | internal/processing/account/update.go | 8 | ||||
| -rw-r--r-- | internal/processing/status/util.go | 17 | ||||
| -rw-r--r-- | internal/text/formatter.go | 4 | ||||
| -rw-r--r-- | internal/typeutils/internaltofrontend.go | 6 | ||||
| -rw-r--r-- | internal/typeutils/internaltofrontend_test.go | 11 | ||||
| -rw-r--r-- | internal/validate/account_test.go | 1 | ||||
| -rw-r--r-- | internal/validate/formvalidation.go | 14 | 
17 files changed, 259 insertions, 21 deletions
diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml index 334acb93b..f33318f61 100644 --- a/docs/api/swagger.yaml +++ b/docs/api/swagger.yaml @@ -138,6 +138,10 @@ definitions:          description: Whether new statuses should be marked sensitive by default.          type: boolean          x-go-name: Sensitive +      status_format: +        description: The default posting format for new statuses. +        type: string +        x-go-name: StatusFormat      title: Source represents display or publishing preferences of user's own account.      type: object      x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model @@ -1941,6 +1945,10 @@ definitions:          description: Mark authored statuses as sensitive by default.          type: boolean          x-go-name: Sensitive +      status_format: +        description: Default format for authored statuses (plain or markdown). +        type: string +        x-go-name: StatusFormat      title: UpdateSource is to be used specifically in an UpdateCredentialsRequest.      type: object      x-go-name: UpdateSource @@ -2576,6 +2584,10 @@ paths:          in: formData          name: source[language]          type: string +      - description: Default format to use for authored statuses (plain or markdown). +        in: formData +        name: source[status_format] +        type: string        produces:        - application/json        responses: diff --git a/internal/api/client/account/accountupdate.go b/internal/api/client/account/accountupdate.go index 786aefb38..3ba214ed1 100644 --- a/internal/api/client/account/accountupdate.go +++ b/internal/api/client/account/accountupdate.go @@ -88,6 +88,10 @@ import (  //   in: formData  //   description: Default language to use for authored statuses (ISO 6391).  //   type: string +// - name: source[status_format] +//   in: formData +//   description: Default format to use for authored statuses (plain or markdown). +//   type: string  //  // security:  // - OAuth2 Bearer: @@ -163,6 +167,10 @@ func parseUpdateAccountForm(c *gin.Context) (*model.UpdateCredentialsRequest, er  		form.Source.Language = &language  	} +	if statusFormat, ok := sourceMap["status_format"]; ok { +		form.Source.StatusFormat = &statusFormat +	} +  	if form == nil ||  		(form.Discoverable == nil &&  			form.Bot == nil && @@ -174,6 +182,7 @@ func parseUpdateAccountForm(c *gin.Context) (*model.UpdateCredentialsRequest, er  			form.Source.Privacy == nil &&  			form.Source.Sensitive == nil &&  			form.Source.Language == nil && +			form.Source.StatusFormat == nil &&  			form.FieldsAttributes == nil) {  		return nil, errors.New("empty form submitted")  	} diff --git a/internal/api/client/account/accountupdate_test.go b/internal/api/client/account/accountupdate_test.go index 91f886721..d59cd02a5 100644 --- a/internal/api/client/account/accountupdate_test.go +++ b/internal/api/client/account/accountupdate_test.go @@ -362,6 +362,81 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpd  	suite.True(apimodelAccount.Locked)  } +func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpdateStatusFormatOK() { +	// set up the request +	// we're updating the language of zork +	requestBody, w, err := testrig.CreateMultipartFormData( +		"", "", +		map[string]string{ +			"source[status_format]": "markdown", +		}) +	if err != nil { +		panic(err) +	} +	bodyBytes := requestBody.Bytes() +	recorder := httptest.NewRecorder() +	ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, account.UpdateCredentialsPath, w.FormDataContentType()) + +	// call the handler +	suite.accountModule.AccountUpdateCredentialsPATCHHandler(ctx) + +	// 1. we should have OK because our request was valid +	suite.Equal(http.StatusOK, recorder.Code) + +	// 2. we should have no error message in the result body +	result := recorder.Result() +	defer result.Body.Close() + +	// check the response +	b, err := ioutil.ReadAll(result.Body) +	suite.NoError(err) + +	// unmarshal the returned account +	apimodelAccount := &apimodel.Account{} +	err = json.Unmarshal(b, apimodelAccount) +	suite.NoError(err) + +	// check the returned api model account +	// fields should be updated +	suite.Equal("markdown", apimodelAccount.Source.StatusFormat) + +	dbAccount, err := suite.db.GetAccountByID(context.Background(), suite.testAccounts["local_account_1"].ID) +	if err != nil { +		suite.FailNow(err.Error()) +	} +	suite.Equal(dbAccount.StatusFormat, "markdown") +} + +func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpdateStatusFormatBad() { +	// set up the request +	// we're updating the language of zork +	requestBody, w, err := testrig.CreateMultipartFormData( +		"", "", +		map[string]string{ +			"source[status_format]": "peepeepoopoo", +		}) +	if err != nil { +		panic(err) +	} +	bodyBytes := requestBody.Bytes() +	recorder := httptest.NewRecorder() +	ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, account.UpdateCredentialsPath, w.FormDataContentType()) + +	// call the handler +	suite.accountModule.AccountUpdateCredentialsPATCHHandler(ctx) + +	suite.Equal(http.StatusBadRequest, recorder.Code) + +	result := recorder.Result() +	defer result.Body.Close() + +	// check the response +	b, err := ioutil.ReadAll(result.Body) +	suite.NoError(err) + +	suite.Equal(`{"error":"Bad Request: status format 'peepeepoopoo' was not recognized, valid options are 'plain', 'markdown'"}`, string(b)) +} +  func TestAccountUpdateTestSuite(t *testing.T) {  	suite.Run(t, new(AccountUpdateTestSuite))  } diff --git a/internal/api/client/status/statuscreate_test.go b/internal/api/client/status/statuscreate_test.go index 93fe74175..a42654a42 100644 --- a/internal/api/client/status/statuscreate_test.go +++ b/internal/api/client/status/statuscreate_test.go @@ -41,13 +41,11 @@ type StatusCreateTestSuite struct {  	StatusStandardTestSuite  } -var statusWithLinksAndTags = `#test alright, should be able to post #links with fragments in them now, let's see........ - -https://docs.gotosocial.org/en/latest/user_guide/posts/#links - -#gotosocial - -(tobi remember to pull the docker image challenge)` +const ( +	statusWithLinksAndTags = "#test alright, should be able to post #links with fragments in them now, let's see........\n\nhttps://docs.gotosocial.org/en/latest/user_guide/posts/#links\n\n#gotosocial\n\n(tobi remember to pull the docker image challenge)" +	statusMarkdown         = "# Title\n\n## Smaller title\n\nThis is a post written in [markdown](https://www.markdownguide.org/)\n\n<img src=\"https://d33wubrfki0l68.cloudfront.net/f1f475a6fda1c2c4be4cac04033db5c3293032b4/513a4/assets/images/markdown-mark-white.svg\"/>" +	statusMarkdownExpected = "<h1>Title</h1>\n\n<h2>Smaller title</h2>\n\n<p>This is a post written in <a href=\"https://www.markdownguide.org/\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">markdown</a></p>\n\n<p><img src=\"https://d33wubrfki0l68.cloudfront.net/f1f475a6fda1c2c4be4cac04033db5c3293032b4/513a4/assets/images/markdown-mark-white.svg\" crossorigin=\"anonymous\"/></p>\n" +)  // Post a new status with some custom visibility settings  func (suite *StatusCreateTestSuite) TestPostNewStatus() { @@ -104,6 +102,49 @@ func (suite *StatusCreateTestSuite) TestPostNewStatus() {  	suite.Equal(statusReply.Account.ID, gtsTag.FirstSeenFromAccountID)  } +func (suite *StatusCreateTestSuite) TestPostNewStatusMarkdown() { +	// set default post language of account 1 to markdown +	testAccount := suite.testAccounts["local_account_1"] +	testAccount.StatusFormat = "markdown" + +	a, err := suite.db.UpdateAccount(context.Background(), testAccount) +	if err != nil { +		suite.FailNow(err.Error()) +	} +	suite.Equal(a.StatusFormat, "markdown") + +	t := suite.testTokens["local_account_1"] +	oauthToken := oauth.DBTokenToToken(t) + +	recorder := httptest.NewRecorder() +	ctx, _ := testrig.CreateGinTestContext(recorder, nil) +	ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) +	ctx.Set(oauth.SessionAuthorizedToken, oauthToken) +	ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) +	ctx.Set(oauth.SessionAuthorizedAccount, a) + +	ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", status.BasePath), nil) +	ctx.Request.Header.Set("accept", "application/json") +	ctx.Request.Form = url.Values{ +		"status":     {statusMarkdown}, +		"visibility": {string(model.VisibilityPublic)}, +	} +	suite.statusModule.StatusCreatePOSTHandler(ctx) + +	suite.EqualValues(http.StatusOK, recorder.Code) + +	result := recorder.Result() +	defer result.Body.Close() +	b, err := ioutil.ReadAll(result.Body) +	suite.NoError(err) + +	statusReply := &model.Status{} +	err = json.Unmarshal(b, statusReply) +	suite.NoError(err) + +	suite.Equal(statusMarkdownExpected, statusReply.Content) +} +  // mention an account that is not yet known to the instance -- it should be looked up and put in the db  func (suite *StatusCreateTestSuite) TestMentionUnknownAccount() {  	// first remove remote account 1 from the database so it gets looked up again diff --git a/internal/api/model/account.go b/internal/api/model/account.go index 4ff229589..dc6fa24b8 100644 --- a/internal/api/model/account.go +++ b/internal/api/model/account.go @@ -163,6 +163,8 @@ type UpdateSource struct {  	Sensitive *bool `form:"sensitive" json:"sensitive" xml:"sensitive"`  	// Default language to use for authored statuses. (ISO 6391)  	Language *string `form:"language" json:"language" xml:"language"` +	// Default format for authored statuses (plain or markdown). +	StatusFormat *string `form:"status_format" json:"status_format" xml:"status_format"`  }  // UpdateField is to be used specifically in an UpdateCredentialsRequest. diff --git a/internal/api/model/source.go b/internal/api/model/source.go index 0ca8f2a6b..14f4ba7a9 100644 --- a/internal/api/model/source.go +++ b/internal/api/model/source.go @@ -31,6 +31,8 @@ type Source struct {  	Sensitive bool `json:"sensitive,omitempty"`  	// The default posting language for new statuses.  	Language string `json:"language,omitempty"` +	// The default posting format for new statuses. +	StatusFormat string `json:"status_format"`  	// Profile bio.  	Note string `json:"note"`  	// Metadata about the account. diff --git a/internal/api/model/status.go b/internal/api/model/status.go index 62efaf434..d3c7a0e4f 100644 --- a/internal/api/model/status.go +++ b/internal/api/model/status.go @@ -181,8 +181,8 @@ type StatusCreateRequest struct {  	Language string `form:"language" json:"language" xml:"language"`  	// Format to use when parsing this status.  	// enum: -	// - markdown  	// - plain +	// - markdown  	// in: formData  	Format StatusFormat `form:"format" json:"format" xml:"format"`  } @@ -245,11 +245,9 @@ type AdvancedVisibilityFlagsForm struct {  // example: plain  type StatusFormat string -// StatusFormatPlain expects a plaintext status which will then be formatted into html. -const StatusFormatPlain StatusFormat = "plain" - -// StatusFormatMarkdown expects a markdown formatted status, which will then be formatted into html. -const StatusFormatMarkdown StatusFormat = "markdown" - -// StatusFormatDefault is the format that should be used when nothing else is specified. -const StatusFormatDefault StatusFormat = StatusFormatPlain +// Format to use when parsing submitted status into an html-formatted status +const ( +	StatusFormatPlain    StatusFormat = "plain" +	StatusFormatMarkdown StatusFormat = "markdown" +	StatusFormatDefault  StatusFormat = StatusFormatPlain +) diff --git a/internal/cache/account.go b/internal/cache/account.go index 7dbbd99b3..a71274d1c 100644 --- a/internal/cache/account.go +++ b/internal/cache/account.go @@ -114,6 +114,7 @@ func copyAccount(account *gtsmodel.Account) *gtsmodel.Account {  		Privacy:                 account.Privacy,  		Sensitive:               account.Sensitive,  		Language:                account.Language, +		StatusFormat:            account.StatusFormat,  		URI:                     account.URI,  		URL:                     account.URL,  		LastWebfingeredAt:       account.LastWebfingeredAt, diff --git a/internal/db/bundb/migrations/20220804120132_account_default_post_format.go b/internal/db/bundb/migrations/20220804120132_account_default_post_format.go new file mode 100644 index 000000000..36e47f73b --- /dev/null +++ b/internal/db/bundb/migrations/20220804120132_account_default_post_format.go @@ -0,0 +1,46 @@ +/* +   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 <http://www.gnu.org/licenses/>. +*/ + +package migrations + +import ( +	"context" +	"strings" + +	"github.com/uptrace/bun" +) + +func init() { +	up := func(ctx context.Context, db *bun.DB) error { +		_, err := db.ExecContext(ctx, "ALTER TABLE ? ADD COLUMN ? TEXT", bun.Ident("accounts"), bun.Ident("status_format")) +		if err != nil && !(strings.Contains(err.Error(), "already exists") || strings.Contains(err.Error(), "duplicate column name") || strings.Contains(err.Error(), "SQLSTATE 42701")) { +			return err +		} +		return nil +	} + +	down := func(ctx context.Context, db *bun.DB) error { +		return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { +			return nil +		}) +	} + +	if err := Migrations.Register(up, down); err != nil { +		panic(err) +	} +} diff --git a/internal/gtsmodel/account.go b/internal/gtsmodel/account.go index 13b9d2cf4..9dcb7bd5b 100644 --- a/internal/gtsmodel/account.go +++ b/internal/gtsmodel/account.go @@ -54,6 +54,7 @@ type Account struct {  	Privacy                 Visibility       `validate:"required_without=Domain,omitempty,oneof=public unlocked followers_only mutuals_only direct" bun:",nullzero"` // Default post privacy for this account  	Sensitive               bool             `validate:"-" bun:",default:false"`                                                                                     // Set posts from this account to sensitive by default?  	Language                string           `validate:"omitempty,bcp47_language_tag" bun:",nullzero,notnull,default:'en'"`                                          // What language does this account post in? +	StatusFormat            string           `validate:"required_without=Domain,omitempty,oneof=plain markdown" bun:",nullzero"`                                     // What is the default format for statuses posted by this account (only for local accounts).  	URI                     string           `validate:"required,url" bun:",nullzero,notnull,unique"`                                                                // ActivityPub URI for this account.  	URL                     string           `validate:"required_without=Domain,omitempty,url" bun:",nullzero,unique"`                                               // Web URL for this account's profile  	LastWebfingeredAt       time.Time        `validate:"required_with=Domain" bun:"type:timestamptz,nullzero"`                                                       // Last time this account was refreshed/located with webfinger. diff --git a/internal/processing/account/update.go b/internal/processing/account/update.go index 78791dcce..804e7ba7e 100644 --- a/internal/processing/account/update.go +++ b/internal/processing/account/update.go @@ -114,6 +114,14 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form  			privacy := p.tc.APIVisToVis(apimodel.Visibility(*form.Source.Privacy))  			account.Privacy = privacy  		} + +		if form.Source.StatusFormat != nil { +			if err := validate.StatusFormat(*form.Source.StatusFormat); err != nil { +				return nil, gtserror.NewErrorBadRequest(err, err.Error()) +			} + +			account.StatusFormat = *form.Source.StatusFormat +		}  	}  	updatedAccount, err := p.db.UpdateAccount(ctx, account) diff --git a/internal/processing/status/util.go b/internal/processing/status/util.go index 52214e95f..64b496673 100644 --- a/internal/processing/status/util.go +++ b/internal/processing/status/util.go @@ -23,6 +23,7 @@ import (  	"errors"  	"fmt" +	"github.com/superseriousbusiness/gotosocial/internal/api/model"  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtserror" @@ -269,9 +270,21 @@ func (p *processor) ProcessContent(ctx context.Context, form *apimodel.AdvancedS  		return nil  	} -	// if format wasn't specified we should set the default +	// if format wasn't specified we should try to figure out what format this user prefers  	if form.Format == "" { -		form.Format = apimodel.StatusFormatDefault +		acct, err := p.db.GetAccountByID(ctx, accountID) +		if err != nil { +			return fmt.Errorf("error processing new content: couldn't retrieve account from db to check post format: %s", err) +		} + +		switch acct.StatusFormat { +		case "plain": +			form.Format = model.StatusFormatPlain +		case "markdown": +			form.Format = model.StatusFormatMarkdown +		default: +			form.Format = model.StatusFormatDefault +		}  	}  	// parse content out of the status depending on what format has been submitted diff --git a/internal/text/formatter.go b/internal/text/formatter.go index 957c09f08..3970d0c72 100644 --- a/internal/text/formatter.go +++ b/internal/text/formatter.go @@ -27,10 +27,10 @@ import (  // Formatter wraps some logic and functions for parsing statuses and other text input into nice html.  type Formatter interface { -	// FromMarkdown parses an HTML text from a markdown-formatted text. -	FromMarkdown(ctx context.Context, md string, mentions []*gtsmodel.Mention, tags []*gtsmodel.Tag) string  	// FromPlain parses an HTML text from a plaintext.  	FromPlain(ctx context.Context, plain string, mentions []*gtsmodel.Mention, tags []*gtsmodel.Tag) string +	// FromMarkdown parses an HTML text from a markdown-formatted text. +	FromMarkdown(ctx context.Context, md string, mentions []*gtsmodel.Mention, tags []*gtsmodel.Tag) string  	// ReplaceTags takes a piece of text and a slice of tags, and returns the same text with the tags nicely formatted as hrefs.  	ReplaceTags(ctx context.Context, in string, tags []*gtsmodel.Tag) string diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 81dfaf9dd..da124ce6c 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -53,10 +53,16 @@ func (c *converter) AccountToAPIAccountSensitive(ctx context.Context, a *gtsmode  		frc = len(frs)  	} +	statusFormat := string(model.StatusFormatDefault) +	if a.StatusFormat != "" { +		statusFormat = a.StatusFormat +	} +  	apiAccount.Source = &model.Source{  		Privacy:             c.VisToAPIVis(ctx, a.Privacy),  		Sensitive:           a.Sensitive,  		Language:            a.Language, +		StatusFormat:        statusFormat,  		Note:                a.NoteRaw,  		Fields:              apiAccount.Fields,  		FollowRequestsCount: frc, diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go index 2350e64a2..6be18d854 100644 --- a/internal/typeutils/internaltofrontend_test.go +++ b/internal/typeutils/internaltofrontend_test.go @@ -43,6 +43,17 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontend() {  	suite.Equal(`{"id":"01F8MH1H7YV1Z7D2C8K2730QBF","username":"the_mighty_zork","acct":"the_mighty_zork","display_name":"original zork (he/they)","locked":false,"bot":false,"created_at":"2022-05-20T11:09:18.000Z","note":"\u003cp\u003ehey yo this is my profile!\u003c/p\u003e","url":"http://localhost:8080/@the_mighty_zork","avatar":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpeg","avatar_static":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpeg","header":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpeg","header_static":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpeg","followers_count":2,"following_count":2,"statuses_count":5,"last_status_at":"2022-05-20T11:37:55.000Z","emojis":[],"fields":[]}`, string(b))  } +func (suite *InternalToFrontendTestSuite) TestAccountToFrontendSensitive() { +	testAccount := suite.testAccounts["local_account_1"] // take zork for this test +	apiAccount, err := suite.typeconverter.AccountToAPIAccountSensitive(context.Background(), testAccount) +	suite.NoError(err) +	suite.NotNil(apiAccount) + +	b, err := json.Marshal(apiAccount) +	suite.NoError(err) +	suite.Equal(`{"id":"01F8MH1H7YV1Z7D2C8K2730QBF","username":"the_mighty_zork","acct":"the_mighty_zork","display_name":"original zork (he/they)","locked":false,"bot":false,"created_at":"2022-05-20T11:09:18.000Z","note":"\u003cp\u003ehey yo this is my profile!\u003c/p\u003e","url":"http://localhost:8080/@the_mighty_zork","avatar":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpeg","avatar_static":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpeg","header":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpeg","header_static":"http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpeg","followers_count":2,"following_count":2,"statuses_count":5,"last_status_at":"2022-05-20T11:37:55.000Z","emojis":[],"fields":[],"source":{"privacy":"public","language":"en","status_format":"plain","note":"hey yo this is my profile!","fields":[]}}`, string(b)) +} +  func (suite *InternalToFrontendTestSuite) TestStatusToFrontend() {  	testStatus := suite.testStatuses["admin_account_status_1"]  	requestingAccount := suite.testAccounts["local_account_1"] diff --git a/internal/validate/account_test.go b/internal/validate/account_test.go index a023b2bcb..e0dda8e16 100644 --- a/internal/validate/account_test.go +++ b/internal/validate/account_test.go @@ -62,6 +62,7 @@ func happyAccount() *gtsmodel.Account {  		Privacy:                 gtsmodel.VisibilityPublic,  		Sensitive:               false,  		Language:                "en", +		StatusFormat:            "plain",  		URI:                     "http://localhost:8080/users/the_mighty_zork",  		URL:                     "http://localhost:8080/@the_mighty_zork",  		LastWebfingeredAt:       time.Time{}, diff --git a/internal/validate/formvalidation.go b/internal/validate/formvalidation.go index e0c27628b..5ce80ae31 100644 --- a/internal/validate/formvalidation.go +++ b/internal/validate/formvalidation.go @@ -144,7 +144,19 @@ func Privacy(privacy string) error {  	case apimodel.VisibilityDirect, apimodel.VisibilityMutualsOnly, apimodel.VisibilityPrivate, apimodel.VisibilityPublic, apimodel.VisibilityUnlisted:  		return nil  	} -	return fmt.Errorf("privacy %s was not recognized", privacy) +	return fmt.Errorf("privacy '%s' was not recognized, valid options are 'direct', 'mutuals_only', 'private', 'public', 'unlisted'", privacy) +} + +// StatusFormat checks that the desired status format setting is valid. +func StatusFormat(statusFormat string) error { +	if statusFormat == "" { +		return fmt.Errorf("empty string for status format not allowed") +	} +	switch apimodel.StatusFormat(statusFormat) { +	case apimodel.StatusFormatPlain, apimodel.StatusFormatMarkdown: +		return nil +	} +	return fmt.Errorf("status format '%s' was not recognized, valid options are 'plain', 'markdown'", statusFormat)  }  // EmojiShortcode just runs the given shortcode through the regular expression  | 
