mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-30 21:32:25 -05:00 
			
		
		
		
	[feature] Advertise rich text formats, support content_type field (#1370)
* Advertise rich text formats, support content_type field * Update JSON in instance patch tests * Replace format with content_type everywhere * update migration to work with both pg and sqlite * regenerate swagger docs * update instance serialization + tests * fix up * learn to code tobi please, i'm begging you --------- Co-authored-by: tsmethurst <tobi.smethurst@protonmail.com>
This commit is contained in:
		
					parent
					
						
							
								baf933cb9f
							
						
					
				
			
			
				commit
				
					
						e6cde25466
					
				
			
		
					 23 changed files with 244 additions and 96 deletions
				
			
		|  | @ -143,10 +143,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. | ||||
|             status_content_type: | ||||
|                 description: The default posting content type for new statuses. | ||||
|                 type: string | ||||
|                 x-go-name: StatusFormat | ||||
|                 x-go-name: StatusContentType | ||||
|         title: Source represents display or publishing preferences of user's own account. | ||||
|         type: object | ||||
|         x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model | ||||
|  | @ -1240,6 +1240,15 @@ definitions: | |||
|                 format: int64 | ||||
|                 type: integer | ||||
|                 x-go-name: MaxMediaAttachments | ||||
|             supported_mime_types: | ||||
|                 description: List of mime types that it's possible to use for statuses on this instance. | ||||
|                 example: | ||||
|                     - text/plain | ||||
|                     - text/markdown | ||||
|                 items: | ||||
|                     type: string | ||||
|                 type: array | ||||
|                 x-go-name: SupportedMimeTypes | ||||
|         title: InstanceConfigurationStatuses models instance status config parameters. | ||||
|         type: object | ||||
|         x-go-name: InstanceConfigurationStatuses | ||||
|  | @ -2112,12 +2121,12 @@ definitions: | |||
|         x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model | ||||
|     statusCreateRequest: | ||||
|         properties: | ||||
|             format: | ||||
|             content_type: | ||||
|                 description: |- | ||||
|                     Format to use when parsing this status. | ||||
|                     Content type to use when parsing this status. | ||||
|                     in: formData | ||||
|                 type: string | ||||
|                 x-go-name: Format | ||||
|                 x-go-name: ContentType | ||||
|             in_reply_to_id: | ||||
|                 description: |- | ||||
|                     ID of the status being replied to, if status is a reply. | ||||
|  | @ -2463,10 +2472,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). | ||||
|             status_content_type: | ||||
|                 description: Default format for authored statuses (text/plain or text/markdown). | ||||
|                 type: string | ||||
|                 x-go-name: StatusFormat | ||||
|                 x-go-name: StatusContentType | ||||
|         title: UpdateSource is to be used specifically in an UpdateCredentialsRequest. | ||||
|         type: object | ||||
|         x-go-name: UpdateSource | ||||
|  | @ -3081,9 +3090,9 @@ paths: | |||
|                   in: formData | ||||
|                   name: source[language] | ||||
|                   type: string | ||||
|                 - description: Default format to use for authored statuses (plain or markdown). | ||||
|                 - description: Default content type to use for authored statuses (text/plain or text/markdown). | ||||
|                   in: formData | ||||
|                   name: source[status_format] | ||||
|                   name: source[status_content_type] | ||||
|                   type: string | ||||
|                 - description: Custom CSS to use when rendering this account's profile or statuses. String must be no more than 5,000 characters (~5kb). | ||||
|                   in: formData | ||||
|  | @ -4874,11 +4883,11 @@ paths: | |||
|                   name: language | ||||
|                   type: string | ||||
|                   x-go-name: Language | ||||
|                 - description: Format to use when parsing this status. | ||||
|                 - description: Content type to use when parsing this status. | ||||
|                   in: formData | ||||
|                   name: format | ||||
|                   name: content_type | ||||
|                   type: string | ||||
|                   x-go-name: Format | ||||
|                   x-go-name: ContentType | ||||
|                 - description: This status will be federated beyond the local timeline(s). | ||||
|                   in: query | ||||
|                   name: federated | ||||
|  |  | |||
|  | @ -99,9 +99,9 @@ import ( | |||
| //		description: Default language to use for authored statuses (ISO 6391). | ||||
| //		type: string | ||||
| //	- | ||||
| //		name: source[status_format] | ||||
| //		name: source[status_content_type] | ||||
| //		in: formData | ||||
| //		description: Default format to use for authored statuses (plain or markdown). | ||||
| //		description: Default content type to use for authored statuses (text/plain or text/markdown). | ||||
| //		type: string | ||||
| //	- | ||||
| //		name: custom_css | ||||
|  | @ -190,8 +190,8 @@ func parseUpdateAccountForm(c *gin.Context) (*apimodel.UpdateCredentialsRequest, | |||
| 		form.Source.Language = &language | ||||
| 	} | ||||
| 
 | ||||
| 	if statusFormat, ok := sourceMap["status_format"]; ok { | ||||
| 		form.Source.StatusFormat = &statusFormat | ||||
| 	if statusContentType, ok := sourceMap["status_content_type"]; ok { | ||||
| 		form.Source.StatusContentType = &statusContentType | ||||
| 	} | ||||
| 
 | ||||
| 	if form == nil || | ||||
|  | @ -205,7 +205,7 @@ func parseUpdateAccountForm(c *gin.Context) (*apimodel.UpdateCredentialsRequest, | |||
| 			form.Source.Privacy == nil && | ||||
| 			form.Source.Sensitive == nil && | ||||
| 			form.Source.Language == nil && | ||||
| 			form.Source.StatusFormat == nil && | ||||
| 			form.Source.StatusContentType == nil && | ||||
| 			form.FieldsAttributes == nil && | ||||
| 			form.CustomCSS == nil && | ||||
| 			form.EnableRSS == nil) { | ||||
|  |  | |||
|  | @ -414,13 +414,13 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpd | |||
| 	suite.True(apimodelAccount.Locked) | ||||
| } | ||||
| 
 | ||||
| func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpdateStatusFormatOK() { | ||||
| func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpdateStatusContentTypeOK() { | ||||
| 	// set up the request | ||||
| 	// we're updating the language of zork | ||||
| 	requestBody, w, err := testrig.CreateMultipartFormData( | ||||
| 		"", "", | ||||
| 		map[string]string{ | ||||
| 			"source[status_format]": "markdown", | ||||
| 			"source[status_content_type]": "text/markdown", | ||||
| 		}) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
|  | @ -450,22 +450,22 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpd | |||
| 
 | ||||
| 	// check the returned api model account | ||||
| 	// fields should be updated | ||||
| 	suite.Equal("markdown", apimodelAccount.Source.StatusFormat) | ||||
| 	suite.Equal("text/markdown", apimodelAccount.Source.StatusContentType) | ||||
| 
 | ||||
| 	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") | ||||
| 	suite.Equal(dbAccount.StatusContentType, "text/markdown") | ||||
| } | ||||
| 
 | ||||
| func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpdateStatusFormatBad() { | ||||
| func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpdateStatusContentTypeBad() { | ||||
| 	// set up the request | ||||
| 	// we're updating the language of zork | ||||
| 	requestBody, w, err := testrig.CreateMultipartFormData( | ||||
| 		"", "", | ||||
| 		map[string]string{ | ||||
| 			"source[status_format]": "peepeepoopoo", | ||||
| 			"source[status_content_type]": "peepeepoopoo", | ||||
| 		}) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
|  | @ -486,7 +486,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpd | |||
| 	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)) | ||||
| 	suite.Equal(`{"error":"Bad Request: status content type 'peepeepoopoo' was not recognized, valid options are 'text/plain', 'text/markdown'"}`, string(b)) | ||||
| } | ||||
| 
 | ||||
| func TestAccountUpdateTestSuite(t *testing.T) { | ||||
|  |  | |||
|  | @ -90,7 +90,11 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() { | |||
|     "statuses": { | ||||
|       "max_characters": 5000, | ||||
|       "max_media_attachments": 6, | ||||
|       "characters_reserved_per_url": 25 | ||||
|       "characters_reserved_per_url": 25, | ||||
|       "supported_mime_types": [ | ||||
|         "text/plain", | ||||
|         "text/markdown" | ||||
|       ] | ||||
|     }, | ||||
|     "media_attachments": { | ||||
|       "supported_mime_types": [ | ||||
|  | @ -188,7 +192,11 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() { | |||
|     "statuses": { | ||||
|       "max_characters": 5000, | ||||
|       "max_media_attachments": 6, | ||||
|       "characters_reserved_per_url": 25 | ||||
|       "characters_reserved_per_url": 25, | ||||
|       "supported_mime_types": [ | ||||
|         "text/plain", | ||||
|         "text/markdown" | ||||
|       ] | ||||
|     }, | ||||
|     "media_attachments": { | ||||
|       "supported_mime_types": [ | ||||
|  | @ -286,7 +294,11 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() { | |||
|     "statuses": { | ||||
|       "max_characters": 5000, | ||||
|       "max_media_attachments": 6, | ||||
|       "characters_reserved_per_url": 25 | ||||
|       "characters_reserved_per_url": 25, | ||||
|       "supported_mime_types": [ | ||||
|         "text/plain", | ||||
|         "text/markdown" | ||||
|       ] | ||||
|     }, | ||||
|     "media_attachments": { | ||||
|       "supported_mime_types": [ | ||||
|  | @ -435,7 +447,11 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() { | |||
|     "statuses": { | ||||
|       "max_characters": 5000, | ||||
|       "max_media_attachments": 6, | ||||
|       "characters_reserved_per_url": 25 | ||||
|       "characters_reserved_per_url": 25, | ||||
|       "supported_mime_types": [ | ||||
|         "text/plain", | ||||
|         "text/markdown" | ||||
|       ] | ||||
|     }, | ||||
|     "media_attachments": { | ||||
|       "supported_mime_types": [ | ||||
|  | @ -554,7 +570,11 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() { | |||
|     "statuses": { | ||||
|       "max_characters": 5000, | ||||
|       "max_media_attachments": 6, | ||||
|       "characters_reserved_per_url": 25 | ||||
|       "characters_reserved_per_url": 25, | ||||
|       "supported_mime_types": [ | ||||
|         "text/plain", | ||||
|         "text/markdown" | ||||
|       ] | ||||
|     }, | ||||
|     "media_attachments": { | ||||
|       "supported_mime_types": [ | ||||
|  | @ -689,7 +709,11 @@ func (suite *InstancePatchTestSuite) TestInstancePatch9() { | |||
|     "statuses": { | ||||
|       "max_characters": 5000, | ||||
|       "max_media_attachments": 6, | ||||
|       "characters_reserved_per_url": 25 | ||||
|       "characters_reserved_per_url": 25, | ||||
|       "supported_mime_types": [ | ||||
|         "text/plain", | ||||
|         "text/markdown" | ||||
|       ] | ||||
|     }, | ||||
|     "media_attachments": { | ||||
|       "supported_mime_types": [ | ||||
|  |  | |||
|  | @ -105,14 +105,14 @@ func (suite *StatusCreateTestSuite) TestPostNewStatus() { | |||
| func (suite *StatusCreateTestSuite) TestPostNewStatusMarkdown() { | ||||
| 	// set default post language of account 1 to markdown | ||||
| 	testAccount := suite.testAccounts["local_account_1"] | ||||
| 	testAccount.StatusFormat = "markdown" | ||||
| 	testAccount.StatusContentType = "text/markdown" | ||||
| 	a := testAccount | ||||
| 
 | ||||
| 	err := suite.db.UpdateAccount(context.Background(), a) | ||||
| 	if err != nil { | ||||
| 		suite.FailNow(err.Error()) | ||||
| 	} | ||||
| 	suite.Equal(a.StatusFormat, "markdown") | ||||
| 	suite.Equal(a.StatusContentType, "text/markdown") | ||||
| 
 | ||||
| 	t := suite.testTokens["local_account_1"] | ||||
| 	oauthToken := oauth.DBTokenToToken(t) | ||||
|  |  | |||
|  | @ -174,8 +174,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"` | ||||
| 	// Default format for authored statuses (text/plain or text/markdown). | ||||
| 	StatusContentType *string `form:"status_content_type" json:"status_content_type" xml:"status_content_type"` | ||||
| } | ||||
| 
 | ||||
| // UpdateField is to be used specifically in an UpdateCredentialsRequest. | ||||
|  |  | |||
|  | @ -73,6 +73,10 @@ type InstanceConfigurationStatuses struct { | |||
| 	// | ||||
| 	// example: 25 | ||||
| 	CharactersReservedPerURL int `json:"characters_reserved_per_url"` | ||||
| 	// List of mime types that it's possible to use for statuses on this instance. | ||||
| 	// | ||||
| 	// example: ["text/plain","text/markdown"] | ||||
| 	SupportedMimeTypes []string `json:"supported_mime_types,omitempty"` | ||||
| } | ||||
| 
 | ||||
| // InstanceConfigurationMediaAttachments models instance media attachment config parameters. | ||||
|  |  | |||
|  | @ -31,8 +31,8 @@ type Source struct { | |||
| 	Sensitive bool `json:"sensitive"` | ||||
| 	// The default posting language for new statuses. | ||||
| 	Language string `json:"language"` | ||||
| 	// The default posting format for new statuses. | ||||
| 	StatusFormat string `json:"status_format"` | ||||
| 	// The default posting content type for new statuses. | ||||
| 	StatusContentType string `json:"status_content_type"` | ||||
| 	// Profile bio. | ||||
| 	Note string `json:"note"` | ||||
| 	// Metadata about the account. | ||||
|  |  | |||
|  | @ -179,9 +179,9 @@ type StatusCreateRequest struct { | |||
| 	// ISO 639 language code for this status. | ||||
| 	// in: formData | ||||
| 	Language string `form:"language" json:"language" xml:"language"` | ||||
| 	// Format to use when parsing this status. | ||||
| 	// Content type to use when parsing this status. | ||||
| 	// in: formData | ||||
| 	Format StatusFormat `form:"format" json:"format" xml:"format"` | ||||
| 	ContentType StatusContentType `form:"content_type" json:"content_type" xml:"content_type"` | ||||
| } | ||||
| 
 | ||||
| // Visibility models the visibility of a status. | ||||
|  | @ -227,16 +227,16 @@ type AdvancedVisibilityFlagsForm struct { | |||
| 	Likeable *bool `form:"likeable" json:"likeable" xml:"likeable"` | ||||
| } | ||||
| 
 | ||||
| // StatusFormat is the format in which to parse the submitted status. | ||||
| // Can be either plain or markdown. Empty will default to plain. | ||||
| // StatusContentType is the content type with which to parse the submitted status. | ||||
| // Can be either text/plain or text/markdown. Empty will default to text/plain. | ||||
| // | ||||
| // swagger:enum statusFormat | ||||
| // swagger:enum statusContentType | ||||
| // swagger:type string | ||||
| type StatusFormat string | ||||
| type StatusContentType string | ||||
| 
 | ||||
| // Format to use when parsing submitted status into an html-formatted status | ||||
| // Content type to use when parsing submitted status into an html-formatted status | ||||
| const ( | ||||
| 	StatusFormatPlain    StatusFormat = "plain" | ||||
| 	StatusFormatMarkdown StatusFormat = "markdown" | ||||
| 	StatusFormatDefault  StatusFormat = StatusFormatPlain | ||||
| 	StatusContentTypePlain    StatusContentType = "text/plain" | ||||
| 	StatusContentTypeMarkdown StatusContentType = "text/markdown" | ||||
| 	StatusContentTypeDefault                    = StatusContentTypePlain | ||||
| ) | ||||
|  |  | |||
|  | @ -89,7 +89,7 @@ func (suite *BasicTestSuite) TestPutAccountWithBunDefaultFields() { | |||
| 	suite.Empty(a.Privacy) | ||||
| 	suite.False(*a.Sensitive) | ||||
| 	suite.Equal("en", a.Language) | ||||
| 	suite.Empty(a.StatusFormat) | ||||
| 	suite.Empty(a.StatusContentType) | ||||
| 	suite.Equal(testAccount.URI, a.URI) | ||||
| 	suite.Equal(testAccount.URL, a.URL) | ||||
| 	suite.Zero(testAccount.FetchedAt) | ||||
|  |  | |||
|  | @ -0,0 +1,96 @@ | |||
| /* | ||||
|    GoToSocial | ||||
|    Copyright (C) 2021-2023 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" | ||||
| 	"github.com/uptrace/bun/dialect" | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| 	up := func(ctx context.Context, db *bun.DB) error { | ||||
| 		return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { | ||||
| 			// Create a new column for status_content_type. | ||||
| 			if _, err := tx.ExecContext(ctx, "ALTER TABLE ? ADD COLUMN ? TEXT", bun.Ident("accounts"), bun.Ident("status_content_type")); err != nil && | ||||
| 				!(strings.Contains(err.Error(), "already exists") || strings.Contains(err.Error(), "duplicate column name") || strings.Contains(err.Error(), "SQLSTATE 42701")) { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			// Port values into this column from the old status_format column, | ||||
| 			// prepending 'text/' to the value to derive the mime type. | ||||
| 			// | ||||
| 			// Eg, 'plain' status_format becomes 'text/plain' status_content_type. | ||||
| 			q := tx. | ||||
| 				NewUpdate(). | ||||
| 				Table("accounts"). | ||||
| 				Where("? IS NOT NULL", bun.Ident("status_format")) | ||||
| 
 | ||||
| 			// We need to switch here because Postgres and SQLite | ||||
| 			// have different syntaxes for concatenation. | ||||
| 			switch tx.Dialect().Name() { | ||||
| 			case dialect.SQLite: | ||||
| 				q = q. | ||||
| 					Set("? = ? || ?", bun.Ident("status_content_type"), "text/", bun.Ident("status_format")) | ||||
| 			case dialect.PG: | ||||
| 				q = q. | ||||
| 					Set("? = CONCAT(?, ?)", bun.Ident("status_content_type"), "text/", bun.Ident("status_format")) | ||||
| 			default: | ||||
| 				panic("db conn was neither pg not sqlite") | ||||
| 			} | ||||
| 
 | ||||
| 			if _, err := q.Exec(ctx); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			// Drop the old status_format column. | ||||
| 			if _, err := tx.ExecContext(ctx, "ALTER TABLE ? DROP COLUMN ?", bun.Ident("accounts"), bun.Ident("status_format")); err != nil && | ||||
| 				!(strings.Contains(err.Error(), "no such column") || strings.Contains(err.Error(), "does not exist") || strings.Contains(err.Error(), "SQLSTATE 42703")) { | ||||
| 				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 { | ||||
| 			var err 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 | ||||
| 			} | ||||
| 			_, err = db.ExecContext(ctx, "UPDATE ? SET ? = SUBSTR(?, 6) WHERE ? IS NOT NULL", bun.Ident("accounts"), bun.Ident("status_format"), bun.Ident("status_content_type"), bun.Ident("status_content_type")) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			_, err = db.ExecContext(ctx, "ALTER TABLE ? DROP COLUMN ?", bun.Ident("accounts"), bun.Ident("status_content_type")) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			return nil | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
| 	if err := Migrations.Register(up, down); err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| } | ||||
|  | @ -60,7 +60,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). | ||||
| 	StatusContentType       string           `validate:"required_without=Domain,omitempty,oneof=text/plain text/markdown" bun:",nullzero"`                           // What is the default format for statuses posted by this account (only for local accounts). | ||||
| 	CustomCSS               string           `validate:"-" bun:",nullzero"`                                                                                          // Custom CSS that should be displayed for this Account's profile and statuses. | ||||
| 	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 | ||||
|  |  | |||
|  | @ -79,7 +79,7 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form | |||
| 
 | ||||
| 		// Process note to generate a valid HTML representation | ||||
| 		var f text.FormatFunc | ||||
| 		if account.StatusFormat == "markdown" { | ||||
| 		if account.StatusContentType == "text/markdown" { | ||||
| 			f = p.formatter.FromMarkdown | ||||
| 		} else { | ||||
| 			f = p.formatter.FromPlain | ||||
|  | @ -144,12 +144,12 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form | |||
| 			account.Privacy = privacy | ||||
| 		} | ||||
| 
 | ||||
| 		if form.Source.StatusFormat != nil { | ||||
| 			if err := validate.StatusFormat(*form.Source.StatusFormat); err != nil { | ||||
| 		if form.Source.StatusContentType != nil { | ||||
| 			if err := validate.StatusContentType(*form.Source.StatusContentType); err != nil { | ||||
| 				return nil, gtserror.NewErrorBadRequest(err, err.Error()) | ||||
| 			} | ||||
| 
 | ||||
| 			account.StatusFormat = *form.Source.StatusFormat | ||||
| 			account.StatusContentType = *form.Source.StatusContentType | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -122,13 +122,13 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateWithMarkdownNote() { | |||
| 		Note: ¬e, | ||||
| 	} | ||||
| 
 | ||||
| 	// set default post language of account 1 to markdown | ||||
| 	testAccount.StatusFormat = "markdown" | ||||
| 	// set default post content type of account 1 to markdown | ||||
| 	testAccount.StatusContentType = "text/markdown" | ||||
| 
 | ||||
| 	// should get no error from the update function, and an api model account returned | ||||
| 	apiAccount, errWithCode := suite.accountProcessor.Update(context.Background(), testAccount, form) | ||||
| 	// reset test account to avoid breaking other tests | ||||
| 	testAccount.StatusFormat = "plain" | ||||
| 	testAccount.StatusContentType = "text/plain" | ||||
| 	suite.NoError(errWithCode) | ||||
| 	suite.NotNil(apiAccount) | ||||
| 
 | ||||
|  |  | |||
|  | @ -290,32 +290,32 @@ func processContent(ctx context.Context, dbService db.DB, formatter text.Formatt | |||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	// if format wasn't specified we should try to figure out what format this user prefers | ||||
| 	if form.Format == "" { | ||||
| 	// if content type wasn't specified we should try to figure out what content type this user prefers | ||||
| 	if form.ContentType == "" { | ||||
| 		acct, err := dbService.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 = apimodel.StatusFormatPlain | ||||
| 		case "markdown": | ||||
| 			form.Format = apimodel.StatusFormatMarkdown | ||||
| 		switch acct.StatusContentType { | ||||
| 		case "text/plain": | ||||
| 			form.ContentType = apimodel.StatusContentTypePlain | ||||
| 		case "text/markdown": | ||||
| 			form.ContentType = apimodel.StatusContentTypeMarkdown | ||||
| 		default: | ||||
| 			form.Format = apimodel.StatusFormatDefault | ||||
| 			form.ContentType = apimodel.StatusContentTypeDefault | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// parse content out of the status depending on what format has been submitted | ||||
| 	// parse content out of the status depending on what content type has been submitted | ||||
| 	var f text.FormatFunc | ||||
| 	switch form.Format { | ||||
| 	case apimodel.StatusFormatPlain: | ||||
| 	switch form.ContentType { | ||||
| 	case apimodel.StatusContentTypePlain: | ||||
| 		f = formatter.FromPlain | ||||
| 	case apimodel.StatusFormatMarkdown: | ||||
| 	case apimodel.StatusContentTypeMarkdown: | ||||
| 		f = formatter.FromMarkdown | ||||
| 	default: | ||||
| 		return fmt.Errorf("format %s not recognised as a valid status format", form.Format) | ||||
| 		return fmt.Errorf("format %s not recognised as a valid status format", form.ContentType) | ||||
| 	} | ||||
| 	formatted := f(ctx, parseMention, accountID, status.ID, form.Status) | ||||
| 
 | ||||
|  |  | |||
|  | @ -50,7 +50,7 @@ func (suite *StatusCreateTestSuite) TestProcessContentWarningWithQuotationMarks( | |||
| 			Visibility:  apimodel.VisibilityPublic, | ||||
| 			ScheduledAt: "", | ||||
| 			Language:    "en", | ||||
| 			Format:      apimodel.StatusFormatPlain, | ||||
| 			ContentType: apimodel.StatusContentTypePlain, | ||||
| 		}, | ||||
| 		AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ | ||||
| 			Federated: nil, | ||||
|  | @ -84,7 +84,7 @@ func (suite *StatusCreateTestSuite) TestProcessContentWarningWithHTMLEscapedQuot | |||
| 			Visibility:  apimodel.VisibilityPublic, | ||||
| 			ScheduledAt: "", | ||||
| 			Language:    "en", | ||||
| 			Format:      apimodel.StatusFormatPlain, | ||||
| 			ContentType: apimodel.StatusContentTypePlain, | ||||
| 		}, | ||||
| 		AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ | ||||
| 			Federated: nil, | ||||
|  | @ -122,7 +122,7 @@ func (suite *StatusCreateTestSuite) TestProcessStatusMarkdownWithUnderscoreEmoji | |||
| 			Visibility:  apimodel.VisibilityPublic, | ||||
| 			ScheduledAt: "", | ||||
| 			Language:    "en", | ||||
| 			Format:      apimodel.StatusFormatMarkdown, | ||||
| 			ContentType: apimodel.StatusContentTypeMarkdown, | ||||
| 		}, | ||||
| 		AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ | ||||
| 			Federated: nil, | ||||
|  | @ -156,7 +156,7 @@ func (suite *StatusCreateTestSuite) TestProcessStatusMarkdownWithSpoilerTextEmoj | |||
| 			Visibility:  apimodel.VisibilityPublic, | ||||
| 			ScheduledAt: "", | ||||
| 			Language:    "en", | ||||
| 			Format:      apimodel.StatusFormatMarkdown, | ||||
| 			ContentType: apimodel.StatusContentTypeMarkdown, | ||||
| 		}, | ||||
| 		AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ | ||||
| 			Federated: nil, | ||||
|  | @ -194,7 +194,7 @@ func (suite *StatusCreateTestSuite) TestProcessMediaDescriptionTooShort() { | |||
| 			Visibility:  apimodel.VisibilityPublic, | ||||
| 			ScheduledAt: "", | ||||
| 			Language:    "en", | ||||
| 			Format:      apimodel.StatusFormatPlain, | ||||
| 			ContentType: apimodel.StatusContentTypePlain, | ||||
| 		}, | ||||
| 		AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ | ||||
| 			Federated: nil, | ||||
|  |  | |||
|  | @ -111,7 +111,7 @@ func (suite *ImportMinimalTestSuite) TestImportMinimalOK() { | |||
| 	suite.Equal(testAccountBefore.Privacy, testAccountAfter.Privacy) | ||||
| 	suite.Equal(testAccountBefore.Sensitive, testAccountAfter.Sensitive) | ||||
| 	suite.Equal(testAccountBefore.Language, testAccountAfter.Language) | ||||
| 	suite.Equal(testAccountBefore.StatusFormat, testAccountAfter.StatusFormat) | ||||
| 	suite.Equal(testAccountBefore.StatusContentType, testAccountAfter.StatusContentType) | ||||
| 	suite.Equal(testAccountBefore.URI, testAccountAfter.URI) | ||||
| 	suite.Equal(testAccountBefore.URL, testAccountAfter.URL) | ||||
| 	suite.Equal(testAccountBefore.InboxURI, testAccountAfter.InboxURI) | ||||
|  |  | |||
|  | @ -43,7 +43,7 @@ type Account struct { | |||
| 	Privacy               string          `json:"privacy,omitempty" bun:",nullzero"` | ||||
| 	Sensitive             *bool           `json:"sensitive"` | ||||
| 	Language              string          `json:"language,omitempty" bun:",nullzero"` | ||||
| 	StatusFormat          string          `json:"statusFormat,omitempty" bun:",nullzero"` | ||||
| 	StatusContentType     string          `json:"statusContentType,omitempty" bun:",nullzero"` | ||||
| 	URI                   string          `json:"uri" bun:",nullzero"` | ||||
| 	URL                   string          `json:"url" bun:",nullzero"` | ||||
| 	InboxURI              string          `json:"inboxURI" bun:",nullzero"` | ||||
|  |  | |||
|  | @ -46,6 +46,11 @@ const ( | |||
| 	instanceSourceURL                           = "https://github.com/superseriousbusiness/gotosocial" | ||||
| ) | ||||
| 
 | ||||
| var instanceStatusesSupportedMimeTypes = []string{ | ||||
| 	string(apimodel.StatusContentTypePlain), | ||||
| 	string(apimodel.StatusContentTypeMarkdown), | ||||
| } | ||||
| 
 | ||||
| func (c *converter) AccountToAPIAccountSensitive(ctx context.Context, a *gtsmodel.Account) (*apimodel.Account, error) { | ||||
| 	// we can build this sensitive account easily by first getting the public account.... | ||||
| 	apiAccount, err := c.AccountToAPIAccountPublic(ctx, a) | ||||
|  | @ -67,16 +72,16 @@ func (c *converter) AccountToAPIAccountSensitive(ctx context.Context, a *gtsmode | |||
| 		frc = len(frs) | ||||
| 	} | ||||
| 
 | ||||
| 	statusFormat := string(apimodel.StatusFormatDefault) | ||||
| 	if a.StatusFormat != "" { | ||||
| 		statusFormat = a.StatusFormat | ||||
| 	statusContentType := string(apimodel.StatusContentTypeDefault) | ||||
| 	if a.StatusContentType != "" { | ||||
| 		statusContentType = a.StatusContentType | ||||
| 	} | ||||
| 
 | ||||
| 	apiAccount.Source = &apimodel.Source{ | ||||
| 		Privacy:             c.VisToAPIVis(ctx, a.Privacy), | ||||
| 		Sensitive:           *a.Sensitive, | ||||
| 		Language:            a.Language, | ||||
| 		StatusFormat:        statusFormat, | ||||
| 		StatusContentType:   statusContentType, | ||||
| 		Note:                a.NoteRaw, | ||||
| 		Fields:              apiAccount.Fields, | ||||
| 		FollowRequestsCount: frc, | ||||
|  | @ -695,6 +700,7 @@ func (c *converter) InstanceToAPIV1Instance(ctx context.Context, i *gtsmodel.Ins | |||
| 	instance.Configuration.Statuses.MaxCharacters = config.GetStatusesMaxChars() | ||||
| 	instance.Configuration.Statuses.MaxMediaAttachments = config.GetStatusesMediaMaxFiles() | ||||
| 	instance.Configuration.Statuses.CharactersReservedPerURL = instanceStatusesCharactersReservedPerURL | ||||
| 	instance.Configuration.Statuses.SupportedMimeTypes = instanceStatusesSupportedMimeTypes | ||||
| 	instance.Configuration.MediaAttachments.SupportedMimeTypes = media.SupportedMIMETypes | ||||
| 	instance.Configuration.MediaAttachments.ImageSizeLimit = int(config.GetMediaImageMaxSize()) | ||||
| 	instance.Configuration.MediaAttachments.ImageMatrixLimit = instanceMediaAttachmentsImageMatrixLimit | ||||
|  | @ -820,6 +826,7 @@ func (c *converter) InstanceToAPIV2Instance(ctx context.Context, i *gtsmodel.Ins | |||
| 	instance.Configuration.Statuses.MaxCharacters = config.GetStatusesMaxChars() | ||||
| 	instance.Configuration.Statuses.MaxMediaAttachments = config.GetStatusesMediaMaxFiles() | ||||
| 	instance.Configuration.Statuses.CharactersReservedPerURL = instanceStatusesCharactersReservedPerURL | ||||
| 	instance.Configuration.Statuses.SupportedMimeTypes = instanceStatusesSupportedMimeTypes | ||||
| 	instance.Configuration.MediaAttachments.SupportedMimeTypes = media.SupportedMIMETypes | ||||
| 	instance.Configuration.MediaAttachments.ImageSizeLimit = int(config.GetMediaImageMaxSize()) | ||||
| 	instance.Configuration.MediaAttachments.ImageMatrixLimit = instanceMediaAttachmentsImageMatrixLimit | ||||
|  |  | |||
|  | @ -198,7 +198,7 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendSensitive() { | |||
|     "privacy": "public", | ||||
|     "sensitive": false, | ||||
|     "language": "en", | ||||
|     "status_format": "plain", | ||||
|     "status_content_type": "text/plain", | ||||
|     "note": "hey yo this is my profile!", | ||||
|     "fields": [], | ||||
|     "follow_requests_count": 0 | ||||
|  | @ -504,7 +504,11 @@ func (suite *InternalToFrontendTestSuite) TestInstanceV1ToFrontend() { | |||
|     "statuses": { | ||||
|       "max_characters": 5000, | ||||
|       "max_media_attachments": 6, | ||||
|       "characters_reserved_per_url": 25 | ||||
|       "characters_reserved_per_url": 25, | ||||
|       "supported_mime_types": [ | ||||
|         "text/plain", | ||||
|         "text/markdown" | ||||
|       ] | ||||
|     }, | ||||
|     "media_attachments": { | ||||
|       "supported_mime_types": [ | ||||
|  | @ -616,7 +620,11 @@ func (suite *InternalToFrontendTestSuite) TestInstanceV2ToFrontend() { | |||
|     "statuses": { | ||||
|       "max_characters": 5000, | ||||
|       "max_media_attachments": 6, | ||||
|       "characters_reserved_per_url": 25 | ||||
|       "characters_reserved_per_url": 25, | ||||
|       "supported_mime_types": [ | ||||
|         "text/plain", | ||||
|         "text/markdown" | ||||
|       ] | ||||
|     }, | ||||
|     "media_attachments": { | ||||
|       "supported_mime_types": [ | ||||
|  |  | |||
|  | @ -63,7 +63,7 @@ func happyAccount() *gtsmodel.Account { | |||
| 		Privacy:                 gtsmodel.VisibilityPublic, | ||||
| 		Sensitive:               testrig.FalseBool(), | ||||
| 		Language:                "en", | ||||
| 		StatusFormat:            "plain", | ||||
| 		StatusContentType:       "text/plain", | ||||
| 		URI:                     "http://localhost:8080/users/the_mighty_zork", | ||||
| 		URL:                     "http://localhost:8080/@the_mighty_zork", | ||||
| 		FetchedAt:               time.Time{}, | ||||
|  |  | |||
|  | @ -150,16 +150,16 @@ func Privacy(privacy string) error { | |||
| 	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 == "" { | ||||
| // StatusContentType checks that the desired status format setting is valid. | ||||
| func StatusContentType(statusContentType string) error { | ||||
| 	if statusContentType == "" { | ||||
| 		return fmt.Errorf("empty string for status format not allowed") | ||||
| 	} | ||||
| 	switch apimodel.StatusFormat(statusFormat) { | ||||
| 	case apimodel.StatusFormatPlain, apimodel.StatusFormatMarkdown: | ||||
| 	switch apimodel.StatusContentType(statusContentType) { | ||||
| 	case apimodel.StatusContentTypePlain, apimodel.StatusContentTypeMarkdown: | ||||
| 		return nil | ||||
| 	} | ||||
| 	return fmt.Errorf("status format '%s' was not recognized, valid options are 'plain', 'markdown'", statusFormat) | ||||
| 	return fmt.Errorf("status content type '%s' was not recognized, valid options are 'text/plain', 'text/markdown'", statusContentType) | ||||
| } | ||||
| 
 | ||||
| func CustomCSS(customCSS string) error { | ||||
|  |  | |||
|  | @ -53,14 +53,14 @@ function UserSettingsForm({ data }) { | |||
| 		- string source[privacy] | ||||
| 		- bool source[sensitive] | ||||
| 		- string source[language] | ||||
| 		- string source[status_format] | ||||
| 		- string source[status_content_type] | ||||
| 	 */ | ||||
| 
 | ||||
| 	const form = { | ||||
| 		defaultPrivacy: useTextInput("source[privacy]", { source: data, defaultValue: "unlisted" }), | ||||
| 		isSensitive: useBoolInput("source[sensitive]", { source: data }), | ||||
| 		language: useTextInput("source[language]", { source: data, valueSelector: (s) => s.source.language?.toUpperCase() ?? "EN" }), | ||||
| 		format: useTextInput("source[status_format]", { source: data, defaultValue: "plain" }), | ||||
| 		statusContentType: useTextInput("source[status_content_type]", { source: data, defaultValue: "text/plain" }), | ||||
| 	}; | ||||
| 
 | ||||
| 	const [submitForm, result] = useFormSubmit(form, query.useUpdateCredentialsMutation()); | ||||
|  | @ -82,10 +82,10 @@ function UserSettingsForm({ data }) { | |||
| 				}> | ||||
| 					<a href="https://docs.gotosocial.org/en/latest/user_guide/posts/#privacy-settings" target="_blank" className="moreinfolink" rel="noreferrer">Learn more about post privacy settings (opens in a new tab)</a> | ||||
| 				</Select> | ||||
| 				<Select field={form.format} label="Default post (and bio) format" options={ | ||||
| 				<Select field={form.statusContentType} label="Default post (and bio) format" options={ | ||||
| 					<> | ||||
| 						<option value="plain">Plain (default)</option> | ||||
| 						<option value="markdown">Markdown</option> | ||||
| 						<option value="text/plain">Plain (default)</option> | ||||
| 						<option value="text/markdown">Markdown</option> | ||||
| 					</> | ||||
| 				}> | ||||
| 					<a href="https://docs.gotosocial.org/en/latest/user_guide/posts/#input-types" target="_blank" className="moreinfolink" rel="noreferrer">Learn more about post format settings (opens in a new tab)</a> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue