diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml index 38562c8ad..ec0963190 100644 --- a/docs/api/swagger.yaml +++ b/docs/api/swagger.yaml @@ -975,6 +975,8 @@ definitions: description: New account registrations require admin approval. type: boolean x-go-name: ApprovalRequired + configuration: + $ref: '#/definitions/instanceConfiguration' contact_account: $ref: '#/definitions/account' description: @@ -1062,6 +1064,126 @@ definitions: type: object x-go-name: Instance x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + instanceConfiguration: + properties: + media_attachments: + $ref: '#/definitions/instanceConfigurationMediaAttachments' + polls: + $ref: '#/definitions/instanceConfigurationPolls' + statuses: + $ref: '#/definitions/instanceConfigurationStatuses' + title: InstanceConfiguration models instance configuration parameters. + type: object + x-go-name: InstanceConfiguration + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + instanceConfigurationMediaAttachments: + properties: + image_matrix_limit: + description: |- + Max allowed image size in pixels as height*width. + + GtS doesn't set a limit on this, but for compatibility + we give Mastodon's 4096x4096px value here. + example: 16777216 + format: int64 + type: integer + x-go-name: ImageMatrixLimit + image_size_limit: + description: Max allowed image size in bytes + example: 2097152 + format: int64 + type: integer + x-go-name: ImageSizeLimit + supported_mime_types: + description: List of mime types that it's possible to upload to this instance. + example: + - image/jpeg + - image/gif + items: + type: string + type: array + x-go-name: SupportedMimeTypes + video_frame_rate_limit: + description: Max allowed video frame rate. + example: 60 + format: int64 + type: integer + x-go-name: VideoFrameRateLimit + video_matrix_limit: + description: |- + Max allowed video size in pixels as height*width. + + GtS doesn't set a limit on this, but for compatibility + we give Mastodon's 4096x4096px value here. + example: 16777216 + format: int64 + type: integer + x-go-name: VideoMatrixLimit + video_size_limit: + description: Max allowed video size in bytes + example: 10485760 + format: int64 + type: integer + x-go-name: VideoSizeLimit + title: InstanceConfigurationMediaAttachments models instance media attachment + config parameters. + type: object + x-go-name: InstanceConfigurationMediaAttachments + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + instanceConfigurationPolls: + properties: + max_characters_per_option: + description: Number of characters allowed per option in the poll. + example: 50 + format: int64 + type: integer + x-go-name: MaxCharactersPerOption + max_expiration: + description: Maximum expiration time of the poll in seconds. + example: 2629746 + format: int64 + type: integer + x-go-name: MaxExpiration + max_options: + description: Number of options permitted in a poll on this instance. + example: 4 + format: int64 + type: integer + x-go-name: MaxOptions + min_expiration: + description: Minimum expiration time of the poll in seconds. + example: 300 + format: int64 + type: integer + x-go-name: MinExpiration + title: InstanceConfigurationPolls models instance poll config parameters. + type: object + x-go-name: InstanceConfigurationPolls + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + instanceConfigurationStatuses: + properties: + characters_reserved_per_url: + description: Amount of characters that a URL will be compressed to. + example: 999 + format: int64 + type: integer + x-go-name: CharactersReservedPerURL + max_characters: + description: Maximum allowed length of a post on this instance, in characters. + example: 5000 + format: int64 + type: integer + x-go-name: MaxCharacters + max_media_attachments: + description: Max number of attachments allowed on a status. + example: 4 + format: int64 + type: integer + x-go-name: MaxMediaAttachments + title: InstanceConfigurationStatuses models instance status config parameters. + type: object + x-go-name: InstanceConfigurationStatuses + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model instanceURLs: properties: streaming_api: diff --git a/internal/api/client/instance/instancepatch_test.go b/internal/api/client/instance/instancepatch_test.go index 90464d009..0f80627de 100644 --- a/internal/api/client/instance/instancepatch_test.go +++ b/internal/api/client/instance/instancepatch_test.go @@ -63,7 +63,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() { b, err := io.ReadAll(result.Body) suite.NoError(err) - suite.Equal(`{"uri":"http://localhost:8080","title":"Example Instance","description":"","short_description":"","email":"someone@example.org","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[]},"max_toot_chars":5000}`, string(b)) + suite.Equal(`{"uri":"http://localhost:8080","title":"Example Instance","description":"","short_description":"","email":"someone@example.org","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":999},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png"],"image_size_limit":1048576,"image_matrix_limit":16777216,"video_size_limit":5242880,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[]},"max_toot_chars":5000}`, string(b)) } func (suite *InstancePatchTestSuite) TestInstancePatch2() { @@ -93,7 +93,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() { b, err := io.ReadAll(result.Body) suite.NoError(err) - suite.Equal(`{"uri":"http://localhost:8080","title":"Geoff's Instance","description":"","short_description":"","email":"","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"","max_toot_chars":5000}`, string(b)) + suite.Equal(`{"uri":"http://localhost:8080","title":"Geoff's Instance","description":"","short_description":"","email":"","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":999},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png"],"image_size_limit":1048576,"image_matrix_limit":16777216,"video_size_limit":5242880,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"","max_toot_chars":5000}`, string(b)) } func (suite *InstancePatchTestSuite) TestInstancePatch3() { @@ -123,7 +123,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() { b, err := io.ReadAll(result.Body) suite.NoError(err) - 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":2,"status_count":16,"user_count":4},"thumbnail":"","max_toot_chars":5000}`, string(b)) + 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,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":999},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png"],"image_size_limit":1048576,"image_matrix_limit":16777216,"video_size_limit":5242880,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"","max_toot_chars":5000}`, string(b)) } func (suite *InstancePatchTestSuite) TestInstancePatch4() { @@ -214,7 +214,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() { b, err := io.ReadAll(result.Body) suite.NoError(err) - suite.Equal(`{"uri":"http://localhost:8080","title":"localhost:8080","description":"","short_description":"","email":"","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"","max_toot_chars":5000}`, string(b)) + suite.Equal(`{"uri":"http://localhost:8080","title":"localhost:8080","description":"","short_description":"","email":"","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":999},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png"],"image_size_limit":1048576,"image_matrix_limit":16777216,"video_size_limit":5242880,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"","max_toot_chars":5000}`, string(b)) } func (suite *InstancePatchTestSuite) TestInstancePatch7() { diff --git a/internal/api/model/instance.go b/internal/api/model/instance.go index f36713cdc..e01710e34 100644 --- a/internal/api/model/instance.go +++ b/internal/api/model/instance.go @@ -62,6 +62,9 @@ type Instance struct { ApprovalRequired bool `json:"approval_required"` // Invites are enabled on this instance. InvitesEnabled bool `json:"invites_enabled"` + // Configuration object containing values about status limits etc. + // This key/value will be omitted for remote instances. + Configuration *InstanceConfiguration `json:"configuration,omitempty"` // URLs of interest for client applications. URLS *InstanceURLs `json:"urls,omitempty"` // Statistics about the instance: number of posts, accounts, etc. @@ -79,6 +82,94 @@ type Instance struct { MaxTootChars uint `json:"max_toot_chars"` } +// InstanceConfiguration models instance configuration parameters. +// +// swagger:model instanceConfiguration +type InstanceConfiguration struct { + // Instance configuration pertaining to status limits. + Statuses *InstanceConfigurationStatuses `json:"statuses"` + // Instance configuration pertaining to media attachment types + size limits. + MediaAttachments *InstanceConfigurationMediaAttachments `json:"media_attachments"` + // Instance configuration pertaining to poll limits. + Polls *InstanceConfigurationPolls `json:"polls"` +} + +// InstanceConfigurationStatuses models instance status config parameters. +// +// swagger:model instanceConfigurationStatuses +type InstanceConfigurationStatuses struct { + // Maximum allowed length of a post on this instance, in characters. + // + // example: 5000 + MaxCharacters int `json:"max_characters"` + // Max number of attachments allowed on a status. + // + // example: 4 + MaxMediaAttachments int `json:"max_media_attachments"` + // Amount of characters that a URL will be compressed to. + // + // example: 999 + CharactersReservedPerURL int `json:"characters_reserved_per_url"` +} + +// InstanceConfigurationMediaAttachments models instance media attachment config parameters. +// +// swagger:model instanceConfigurationMediaAttachments +type InstanceConfigurationMediaAttachments struct { + // List of mime types that it's possible to upload to this instance. + // + // example: ["image/jpeg","image/gif"] + SupportedMimeTypes []string `json:"supported_mime_types"` + // Max allowed image size in bytes + // + // example: 2097152 + ImageSizeLimit int `json:"image_size_limit"` + // Max allowed image size in pixels as height*width. + // + // GtS doesn't set a limit on this, but for compatibility + // we give Mastodon's 4096x4096px value here. + // + // example: 16777216 + ImageMatrixLimit int `json:"image_matrix_limit"` + // Max allowed video size in bytes + // + // example: 10485760 + VideoSizeLimit int `json:"video_size_limit"` + // Max allowed video frame rate. + // + // example: 60 + VideoFrameRateLimit int `json:"video_frame_rate_limit"` + // Max allowed video size in pixels as height*width. + // + // GtS doesn't set a limit on this, but for compatibility + // we give Mastodon's 4096x4096px value here. + // + // example: 16777216 + VideoMatrixLimit int `json:"video_matrix_limit"` +} + +// InstanceConfigurationPolls models instance poll config parameters. +// +// swagger:model instanceConfigurationPolls +type InstanceConfigurationPolls struct { + // Number of options permitted in a poll on this instance. + // + // example: 4 + MaxOptions int `json:"max_options"` + // Number of characters allowed per option in the poll. + // + // example: 50 + MaxCharactersPerOption int `json:"max_characters_per_option"` + // Minimum expiration time of the poll in seconds. + // + // example: 300 + MinExpiration int `json:"min_expiration"` + // Maximum expiration time of the poll in seconds. + // + // example: 2629746 + MaxExpiration int `json:"max_expiration"` +} + // InstanceURLs models instance-relevant URLs for client application consumption. // // swagger:model instanceURLs diff --git a/internal/media/util.go b/internal/media/util.go index f3cd1b986..6dfcede89 100644 --- a/internal/media/util.go +++ b/internal/media/util.go @@ -26,6 +26,16 @@ import ( "github.com/sirupsen/logrus" ) +// AllSupportedMIMETypes just returns all media +// MIME types supported by this instance. +func AllSupportedMIMETypes() []string { + return []string{ + mimeImageJpeg, + mimeImageGif, + mimeImagePng, + } +} + // parseContentType parses the MIME content type from a file, returning it as a string in the form (eg., "image/jpeg"). // Returns an error if the content type is not something we can process. // diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 774739d85..eb4bcf70d 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -29,6 +29,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/util" ) @@ -595,9 +596,32 @@ func (c *converter) InstanceToAPIInstance(ctx context.Context, i *gtsmodel.Insta mi.InvitesEnabled = false // TODO mi.MaxTootChars = uint(config.GetStatusesMaxChars()) mi.URLS = &model.InstanceURLs{ - StreamingAPI: fmt.Sprintf("wss://%s", host), + StreamingAPI: "wss://" + host, } mi.Version = config.GetSoftwareVersion() + + // todo: remove hardcoded values and put them in config somewhere + mi.Configuration = &model.InstanceConfiguration{ + Statuses: &model.InstanceConfigurationStatuses{ + MaxCharacters: config.GetStatusesMaxChars(), + MaxMediaAttachments: config.GetStatusesMediaMaxFiles(), + CharactersReservedPerURL: 999, + }, + MediaAttachments: &model.InstanceConfigurationMediaAttachments{ + SupportedMimeTypes: media.AllSupportedMIMETypes(), + ImageSizeLimit: config.GetMediaImageMaxSize(), + ImageMatrixLimit: 16777216, // height*width + VideoSizeLimit: config.GetMediaVideoMaxSize(), + VideoFrameRateLimit: 60, + VideoMatrixLimit: 16777216, // height*width + }, + Polls: &model.InstanceConfigurationPolls{ + MaxOptions: config.GetStatusesPollMaxOptions(), + MaxCharactersPerOption: config.GetStatusesPollOptionMaxChars(), + MinExpiration: 300, // seconds + MaxExpiration: 2629746, // seconds + }, + } } // get the instance account if it exists and just skip if it doesn't