From f5689a9e5fa5dbcae6c56fa9f393c2fc4686ac19 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Sat, 6 Aug 2022 12:09:21 +0200 Subject: [PATCH] [feature] Let accounts set default status format, and use this when processing new statuses (#739) * add post_format to acct & use it when making post * update swagger docs * add status_format updating to frontend * fix up tests * post_format => status_format * add status_format to account validation --- docs/api/swagger.yaml | 12 +++ internal/api/client/account/accountupdate.go | 9 +++ .../api/client/account/accountupdate_test.go | 75 +++++++++++++++++++ .../api/client/status/statuscreate_test.go | 55 ++++++++++++-- internal/api/model/account.go | 2 + internal/api/model/source.go | 2 + internal/api/model/status.go | 16 ++-- internal/cache/account.go | 1 + ...20804120132_account_default_post_format.go | 46 ++++++++++++ internal/gtsmodel/account.go | 1 + internal/processing/account/update.go | 8 ++ internal/processing/status/util.go | 17 ++++- internal/text/formatter.go | 4 +- internal/typeutils/internaltofrontend.go | 6 ++ internal/typeutils/internaltofrontend_test.go | 11 +++ internal/validate/account_test.go | 1 + internal/validate/formvalidation.go | 14 +++- 17 files changed, 259 insertions(+), 21 deletions(-) create mode 100644 internal/db/bundb/migrations/20220804120132_account_default_post_format.go 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" + statusMarkdownExpected = "
This is a post written in markdown
\n\n\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