diff options
author | 2023-02-02 14:08:13 +0100 | |
---|---|---|
committer | 2023-02-02 14:08:13 +0100 | |
commit | 382512a5a6cc3f13576bbde8d607098d019f4063 (patch) | |
tree | dc2ccd1d30cd65b3f3d576a8d2a6910bbecc593a /internal/typeutils | |
parent | [chore/performance] use only 1 sqlite db connection regardless of multiplier ... (diff) | |
download | gotosocial-382512a5a6cc3f13576bbde8d607098d019f4063.tar.xz |
[feature] Implement `/api/v2/instance` endpoint (#1409)
* interim: start adding /api/v2/instance
* finish up
Diffstat (limited to 'internal/typeutils')
-rw-r--r-- | internal/typeutils/converter.go | 6 | ||||
-rw-r--r-- | internal/typeutils/internaltofrontend.go | 247 | ||||
-rw-r--r-- | internal/typeutils/internaltofrontend_test.go | 257 |
3 files changed, 352 insertions, 158 deletions
diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go index ec7b09f27..0f741ddb1 100644 --- a/internal/typeutils/converter.go +++ b/internal/typeutils/converter.go @@ -79,8 +79,10 @@ type TypeConverter interface { StatusToAPIStatus(ctx context.Context, s *gtsmodel.Status, requestingAccount *gtsmodel.Account) (*apimodel.Status, error) // VisToAPIVis converts a gts visibility into its api equivalent VisToAPIVis(ctx context.Context, m gtsmodel.Visibility) apimodel.Visibility - // InstanceToAPIInstance converts a gts instance into its api equivalent for serving at /api/v1/instance - InstanceToAPIInstance(ctx context.Context, i *gtsmodel.Instance) (*apimodel.Instance, error) + // InstanceToAPIV1Instance converts a gts instance into its api equivalent for serving at /api/v1/instance + InstanceToAPIV1Instance(ctx context.Context, i *gtsmodel.Instance) (*apimodel.InstanceV1, error) + // InstanceToAPIV2Instance converts a gts instance into its api equivalent for serving at /api/v2/instance + InstanceToAPIV2Instance(ctx context.Context, i *gtsmodel.Instance) (*apimodel.InstanceV2, error) // RelationshipToAPIRelationship converts a gts relationship into its api equivalent for serving in various places RelationshipToAPIRelationship(ctx context.Context, r *gtsmodel.Relationship) (*apimodel.Relationship, error) // NotificationToAPINotification converts a gts notification into a api notification diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 2483fc5ba..799ccb0c4 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -20,7 +20,6 @@ package typeutils import ( "context" - "errors" "fmt" "math" "strconv" @@ -43,6 +42,8 @@ const ( instanceMediaAttachmentsVideoFrameRateLimit = 60 instancePollsMinExpiration = 300 // seconds instancePollsMaxExpiration = 2629746 // seconds + instanceAccountsMaxFeaturedTags = 10 + instanceSourceURL = "https://github.com/superseriousbusiness/gotosocial" ) func (c *converter) AccountToAPIAccountSensitive(ctx context.Context, a *gtsmodel.Account) (*apimodel.Account, error) { @@ -675,113 +676,189 @@ func (c *converter) VisToAPIVis(ctx context.Context, m gtsmodel.Visibility) apim return "" } -func (c *converter) InstanceToAPIInstance(ctx context.Context, i *gtsmodel.Instance) (*apimodel.Instance, error) { - mi := &apimodel.Instance{ +func (c *converter) InstanceToAPIV1Instance(ctx context.Context, i *gtsmodel.Instance) (*apimodel.InstanceV1, error) { + instance := &apimodel.InstanceV1{ URI: i.URI, + AccountDomain: config.GetAccountDomain(), Title: i.Title, Description: i.Description, ShortDescription: i.ShortDescription, Email: i.ContactEmail, - Version: i.Version, - Stats: make(map[string]int), - } - - // if the requested instance is *this* instance, we can add some extra information - if host := config.GetHost(); i.Domain == host { - mi.AccountDomain = config.GetAccountDomain() - - if ia, err := c.db.GetInstanceAccount(ctx, ""); err == nil { - // assume default logo - mi.Thumbnail = config.GetProtocol() + "://" + host + "/assets/logo.png" - - // take instance account avatar as instance thumbnail if we can - if ia.AvatarMediaAttachmentID != "" { - if ia.AvatarMediaAttachment == nil { - avi, err := c.db.GetAttachmentByID(ctx, ia.AvatarMediaAttachmentID) - if err == nil { - ia.AvatarMediaAttachment = avi - } else if !errors.Is(err, db.ErrNoEntries) { - log.Errorf("InstanceToAPIInstance: error getting instance avatar attachment with id %s: %s", ia.AvatarMediaAttachmentID, err) - } - } - - if ia.AvatarMediaAttachment != nil { - mi.Thumbnail = ia.AvatarMediaAttachment.URL - mi.ThumbnailType = ia.AvatarMediaAttachment.File.ContentType - mi.ThumbnailDescription = ia.AvatarMediaAttachment.Description - } + Version: config.GetSoftwareVersion(), + Languages: []string{}, // todo: not supported yet + Registrations: config.GetAccountsRegistrationOpen(), + ApprovalRequired: config.GetAccountsApprovalRequired(), + InvitesEnabled: false, // todo: not supported yet + MaxTootChars: uint(config.GetStatusesMaxChars()), + } + + // configuration + instance.Configuration.Statuses.MaxCharacters = config.GetStatusesMaxChars() + instance.Configuration.Statuses.MaxMediaAttachments = config.GetStatusesMediaMaxFiles() + instance.Configuration.Statuses.CharactersReservedPerURL = instanceStatusesCharactersReservedPerURL + instance.Configuration.MediaAttachments.SupportedMimeTypes = media.SupportedMIMETypes + instance.Configuration.MediaAttachments.ImageSizeLimit = int(config.GetMediaImageMaxSize()) + instance.Configuration.MediaAttachments.ImageMatrixLimit = instanceMediaAttachmentsImageMatrixLimit + instance.Configuration.MediaAttachments.VideoSizeLimit = int(config.GetMediaVideoMaxSize()) + instance.Configuration.MediaAttachments.VideoFrameRateLimit = instanceMediaAttachmentsVideoFrameRateLimit + instance.Configuration.MediaAttachments.VideoMatrixLimit = instanceMediaAttachmentsVideoMatrixLimit + instance.Configuration.Polls.MaxOptions = config.GetStatusesPollMaxOptions() + instance.Configuration.Polls.MaxCharactersPerOption = config.GetStatusesPollOptionMaxChars() + instance.Configuration.Polls.MinExpiration = instancePollsMinExpiration + instance.Configuration.Polls.MaxExpiration = instancePollsMaxExpiration + instance.Configuration.Accounts.AllowCustomCSS = config.GetAccountsAllowCustomCSS() + instance.Configuration.Accounts.MaxFeaturedTags = instanceAccountsMaxFeaturedTags + instance.Configuration.Emojis.EmojiSizeLimit = int(config.GetMediaEmojiLocalMaxSize()) + + // URLs + instance.URLs.StreamingAPI = "wss://" + i.Domain + + // statistics + stats := make(map[string]int, 3) + userCount, err := c.db.CountInstanceUsers(ctx, i.Domain) + if err != nil { + return nil, fmt.Errorf("InstanceToAPIV1Instance: db error getting counting instance users: %w", err) + } + stats["user_count"] = userCount + + statusCount, err := c.db.CountInstanceStatuses(ctx, i.Domain) + if err != nil { + return nil, fmt.Errorf("InstanceToAPIV1Instance: db error getting counting instance statuses: %w", err) + } + stats["status_count"] = statusCount + + domainCount, err := c.db.CountInstanceDomains(ctx, i.Domain) + if err != nil { + return nil, fmt.Errorf("InstanceToAPIV1Instance: db error getting counting instance domains: %w", err) + } + stats["domain_count"] = domainCount + instance.Stats = stats + + // thumbnail + iAccount, err := c.db.GetInstanceAccount(ctx, "") + if err != nil { + return nil, fmt.Errorf("InstanceToAPIV1Instance: db error getting instance account: %w", err) + } + + if iAccount.AvatarMediaAttachmentID != "" { + if iAccount.AvatarMediaAttachment == nil { + avi, err := c.db.GetAttachmentByID(ctx, iAccount.AvatarMediaAttachmentID) + if err != nil { + return nil, fmt.Errorf("InstanceToAPIInstance: error getting instance avatar attachment with id %s: %w", iAccount.AvatarMediaAttachmentID, err) } + iAccount.AvatarMediaAttachment = avi } - userCount, err := c.db.CountInstanceUsers(ctx, host) - if err == nil { - mi.Stats["user_count"] = userCount - } + instance.Thumbnail = iAccount.AvatarMediaAttachment.URL + instance.ThumbnailType = iAccount.AvatarMediaAttachment.File.ContentType + instance.ThumbnailDescription = iAccount.AvatarMediaAttachment.Description + } else { + instance.Thumbnail = config.GetProtocol() + "://" + i.Domain + "/assets/logo.png" // default thumb + } - statusCount, err := c.db.CountInstanceStatuses(ctx, host) - if err == nil { - mi.Stats["status_count"] = statusCount + // contact account + if i.ContactAccountID != "" { + if i.ContactAccount == nil { + contactAccount, err := c.db.GetAccountByID(ctx, i.ContactAccountID) + if err != nil { + return nil, fmt.Errorf("InstanceToAPIV1Instance: db error getting instance contact account %s: %w", i.ContactAccountID, err) + } + i.ContactAccount = contactAccount } - domainCount, err := c.db.CountInstanceDomains(ctx, host) - if err == nil { - mi.Stats["domain_count"] = domainCount + account, err := c.AccountToAPIAccountPublic(ctx, i.ContactAccount) + if err != nil { + return nil, fmt.Errorf("InstanceToAPIV1Instance: error converting instance contact account %s: %w", i.ContactAccountID, err) } + instance.ContactAccount = account + } - mi.Registrations = config.GetAccountsRegistrationOpen() - mi.ApprovalRequired = config.GetAccountsApprovalRequired() - mi.InvitesEnabled = false // TODO - mi.MaxTootChars = uint(config.GetStatusesMaxChars()) - mi.URLS = &apimodel.InstanceURLs{ - StreamingAPI: "wss://" + host, - } - mi.Version = config.GetSoftwareVersion() - - // todo: remove hardcoded values and put them in config somewhere - mi.Configuration = &apimodel.InstanceConfiguration{ - Statuses: &apimodel.InstanceConfigurationStatuses{ - MaxCharacters: config.GetStatusesMaxChars(), - MaxMediaAttachments: config.GetStatusesMediaMaxFiles(), - CharactersReservedPerURL: instanceStatusesCharactersReservedPerURL, - }, - MediaAttachments: &apimodel.InstanceConfigurationMediaAttachments{ - SupportedMimeTypes: media.SupportedMIMETypes, - ImageSizeLimit: int(config.GetMediaImageMaxSize()), // bytes - ImageMatrixLimit: instanceMediaAttachmentsImageMatrixLimit, // height*width - VideoSizeLimit: int(config.GetMediaVideoMaxSize()), // bytes - VideoFrameRateLimit: instanceMediaAttachmentsVideoFrameRateLimit, - VideoMatrixLimit: instanceMediaAttachmentsVideoMatrixLimit, // height*width - }, - Polls: &apimodel.InstanceConfigurationPolls{ - MaxOptions: config.GetStatusesPollMaxOptions(), - MaxCharactersPerOption: config.GetStatusesPollOptionMaxChars(), - MinExpiration: instancePollsMinExpiration, // seconds - MaxExpiration: instancePollsMaxExpiration, // seconds - }, - Accounts: &apimodel.InstanceConfigurationAccounts{ - AllowCustomCSS: config.GetAccountsAllowCustomCSS(), - }, - Emojis: &apimodel.InstanceConfigurationEmojis{ - EmojiSizeLimit: int(config.GetMediaEmojiLocalMaxSize()), // bytes - }, - } + return instance, nil +} + +func (c *converter) InstanceToAPIV2Instance(ctx context.Context, i *gtsmodel.Instance) (*apimodel.InstanceV2, error) { + instance := &apimodel.InstanceV2{ + Domain: i.Domain, + AccountDomain: config.GetAccountDomain(), + Title: i.Title, + Version: config.GetSoftwareVersion(), + SourceURL: instanceSourceURL, + Description: i.Description, + Usage: apimodel.InstanceV2Usage{}, // todo: not implemented + Languages: []string{}, // todo: not implemented + Rules: []interface{}{}, // todo: not implemented + } + + // thumbnail + thumbnail := apimodel.InstanceV2Thumbnail{} + + iAccount, err := c.db.GetInstanceAccount(ctx, "") + if err != nil { + return nil, fmt.Errorf("InstanceToAPIV2Instance: db error getting instance account: %w", err) } - // contact account is optional but let's try to get it + if iAccount.AvatarMediaAttachmentID != "" { + if iAccount.AvatarMediaAttachment == nil { + avi, err := c.db.GetAttachmentByID(ctx, iAccount.AvatarMediaAttachmentID) + if err != nil { + return nil, fmt.Errorf("InstanceToAPIV2Instance: error getting instance avatar attachment with id %s: %w", iAccount.AvatarMediaAttachmentID, err) + } + iAccount.AvatarMediaAttachment = avi + } + + thumbnail.URL = iAccount.AvatarMediaAttachment.URL + thumbnail.Type = iAccount.AvatarMediaAttachment.File.ContentType + thumbnail.Description = iAccount.AvatarMediaAttachment.Description + thumbnail.Blurhash = iAccount.AvatarMediaAttachment.Blurhash + } else { + thumbnail.URL = config.GetProtocol() + "://" + i.Domain + "/assets/logo.png" // default thumb + } + + instance.Thumbnail = thumbnail + + // configuration + instance.Configuration.URLs.Streaming = "wss://" + i.Domain + instance.Configuration.Statuses.MaxCharacters = config.GetStatusesMaxChars() + instance.Configuration.Statuses.MaxMediaAttachments = config.GetStatusesMediaMaxFiles() + instance.Configuration.Statuses.CharactersReservedPerURL = instanceStatusesCharactersReservedPerURL + instance.Configuration.MediaAttachments.SupportedMimeTypes = media.SupportedMIMETypes + instance.Configuration.MediaAttachments.ImageSizeLimit = int(config.GetMediaImageMaxSize()) + instance.Configuration.MediaAttachments.ImageMatrixLimit = instanceMediaAttachmentsImageMatrixLimit + instance.Configuration.MediaAttachments.VideoSizeLimit = int(config.GetMediaVideoMaxSize()) + instance.Configuration.MediaAttachments.VideoFrameRateLimit = instanceMediaAttachmentsVideoFrameRateLimit + instance.Configuration.MediaAttachments.VideoMatrixLimit = instanceMediaAttachmentsVideoMatrixLimit + instance.Configuration.Polls.MaxOptions = config.GetStatusesPollMaxOptions() + instance.Configuration.Polls.MaxCharactersPerOption = config.GetStatusesPollOptionMaxChars() + instance.Configuration.Polls.MinExpiration = instancePollsMinExpiration + instance.Configuration.Polls.MaxExpiration = instancePollsMaxExpiration + instance.Configuration.Accounts.AllowCustomCSS = config.GetAccountsAllowCustomCSS() + instance.Configuration.Accounts.MaxFeaturedTags = instanceAccountsMaxFeaturedTags + instance.Configuration.Emojis.EmojiSizeLimit = int(config.GetMediaEmojiLocalMaxSize()) + + // registrations + instance.Registrations.Enabled = config.GetAccountsRegistrationOpen() + instance.Registrations.ApprovalRequired = config.GetAccountsApprovalRequired() + instance.Registrations.Message = nil // todo: not implemented + + // contact + instance.Contact.Email = i.ContactEmail if i.ContactAccountID != "" { if i.ContactAccount == nil { contactAccount, err := c.db.GetAccountByID(ctx, i.ContactAccountID) - if err == nil { - i.ContactAccount = contactAccount + if err != nil { + return nil, fmt.Errorf("InstanceToAPIV2Instance: db error getting instance contact account %s: %w", i.ContactAccountID, err) } + i.ContactAccount = contactAccount } - ma, err := c.AccountToAPIAccountPublic(ctx, i.ContactAccount) - if err == nil { - mi.ContactAccount = ma + + account, err := c.AccountToAPIAccountPublic(ctx, i.ContactAccount) + if err != nil { + return nil, fmt.Errorf("InstanceToAPIV2Instance: error converting instance contact account %s: %w", i.ContactAccountID, err) } + instance.Contact.Account = account } - return mi, nil + return instance, nil } func (c *converter) RelationshipToAPIRelationship(ctx context.Context, r *gtsmodel.Relationship) (*apimodel.Relationship, error) { diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go index 0c888a521..0704fb555 100644 --- a/internal/typeutils/internaltofrontend_test.go +++ b/internal/typeutils/internaltofrontend_test.go @@ -24,8 +24,9 @@ import ( "testing" "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/testrig" ) type InternalToFrontendTestSuite struct { @@ -454,93 +455,207 @@ func (suite *InternalToFrontendTestSuite) TestVideoAttachmentToFrontend() { }`, string(b)) } -func (suite *InternalToFrontendTestSuite) TestInstanceToFrontend() { - testInstance := >smodel.Instance{ - CreatedAt: testrig.TimeMustParse("2021-10-20T11:36:45Z"), - UpdatedAt: testrig.TimeMustParse("2021-10-20T11:36:45Z"), - Domain: "example.org", - Title: "example instance", - URI: "https://example.org", - ShortDescription: "a little description", - Description: "a much longer description", - ContactEmail: "someone@example.org", - Version: "software-from-hell 0.666", +func (suite *InternalToFrontendTestSuite) TestInstanceV1ToFrontend() { + ctx := context.Background() + + i := >smodel.Instance{} + if err := suite.db.GetWhere(ctx, []db.Where{{Key: "domain", Value: config.GetHost()}}, i); err != nil { + suite.FailNow(err.Error()) } - apiInstance, err := suite.typeconverter.InstanceToAPIInstance(context.Background(), testInstance) - suite.NoError(err) + instance, err := suite.typeconverter.InstanceToAPIV1Instance(ctx, i) + if err != nil { + suite.FailNow(err.Error()) + } - b, err := json.MarshalIndent(apiInstance, "", " ") + b, err := json.MarshalIndent(instance, "", " ") suite.NoError(err) suite.Equal(`{ - "uri": "https://example.org", - "title": "example instance", - "description": "a much longer description", - "short_description": "a little description", - "email": "someone@example.org", - "version": "software-from-hell 0.666", - "registrations": false, - "approval_required": false, + "uri": "http://localhost:8080", + "account_domain": "localhost:8080", + "title": "GoToSocial Testrig Instance", + "description": "\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e", + "short_description": "\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e", + "email": "admin@example.org", + "version": "0.0.0-testrig", + "registrations": true, + "approval_required": true, "invites_enabled": false, - "thumbnail": "", - "max_toot_chars": 0 + "configuration": { + "statuses": { + "max_characters": 5000, + "max_media_attachments": 6, + "characters_reserved_per_url": 25 + }, + "media_attachments": { + "supported_mime_types": [ + "image/jpeg", + "image/gif", + "image/png", + "image/webp", + "video/mp4" + ], + "image_size_limit": 10485760, + "image_matrix_limit": 16777216, + "video_size_limit": 41943040, + "video_frame_rate_limit": 60, + "video_matrix_limit": 16777216 + }, + "polls": { + "max_options": 6, + "max_characters_per_option": 50, + "min_expiration": 300, + "max_expiration": 2629746 + }, + "accounts": { + "allow_custom_css": true, + "max_featured_tags": 10 + }, + "emojis": { + "emoji_size_limit": 51200 + } + }, + "urls": { + "streaming_api": "wss://localhost:8080" + }, + "stats": { + "domain_count": 2, + "status_count": 16, + "user_count": 4 + }, + "thumbnail": "http://localhost:8080/assets/logo.png", + "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": "http://localhost:8080/assets/default_header.png", + "header_static": "http://localhost:8080/assets/default_header.png", + "followers_count": 1, + "following_count": 1, + "statuses_count": 4, + "last_status_at": "2021-10-20T10:41:37.000Z", + "emojis": [], + "fields": [], + "enable_rss": true, + "role": "admin" + }, + "max_toot_chars": 5000 }`, string(b)) } -func (suite *InternalToFrontendTestSuite) TestInstanceToFrontendWithAdminAccount() { - testInstance := >smodel.Instance{ - CreatedAt: testrig.TimeMustParse("2021-10-20T11:36:45Z"), - UpdatedAt: testrig.TimeMustParse("2021-10-20T11:36:45Z"), - Domain: "example.org", - Title: "example instance", - URI: "https://example.org", - ShortDescription: "a little description", - Description: "a much longer description", - ContactEmail: "someone@example.org", - ContactAccountID: suite.testAccounts["remote_account_2"].ID, - Version: "software-from-hell 0.666", +func (suite *InternalToFrontendTestSuite) TestInstanceV2ToFrontend() { + ctx := context.Background() + + i := >smodel.Instance{} + if err := suite.db.GetWhere(ctx, []db.Where{{Key: "domain", Value: config.GetHost()}}, i); err != nil { + suite.FailNow(err.Error()) } - apiInstance, err := suite.typeconverter.InstanceToAPIInstance(context.Background(), testInstance) - suite.NoError(err) + instance, err := suite.typeconverter.InstanceToAPIV2Instance(ctx, i) + if err != nil { + suite.FailNow(err.Error()) + } - b, err := json.MarshalIndent(apiInstance, "", " ") + b, err := json.MarshalIndent(instance, "", " ") suite.NoError(err) suite.Equal(`{ - "uri": "https://example.org", - "title": "example instance", - "description": "a much longer description", - "short_description": "a little description", - "email": "someone@example.org", - "version": "software-from-hell 0.666", - "registrations": false, - "approval_required": false, - "invites_enabled": false, - "thumbnail": "", - "contact_account": { - "id": "01FHMQX3GAABWSM0S2VZEC2SWC", - "username": "Some_User", - "acct": "Some_User@example.org", - "display_name": "some user", - "locked": true, - "bot": false, - "created_at": "2020-08-10T12:13:28.000Z", - "note": "i'm a real son of a gun", - "url": "http://example.org/@Some_User", - "avatar": "", - "avatar_static": "", - "header": "http://localhost:8080/assets/default_header.png", - "header_static": "http://localhost:8080/assets/default_header.png", - "followers_count": 0, - "following_count": 0, - "statuses_count": 0, - "last_status_at": null, - "emojis": [], - "fields": [] + "domain": "localhost:8080", + "account_domain": "localhost:8080", + "title": "GoToSocial Testrig Instance", + "version": "0.0.0-testrig", + "source_url": "https://github.com/superseriousbusiness/gotosocial", + "description": "\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e", + "usage": { + "users": { + "active_month": 0 + } + }, + "thumbnail": { + "url": "http://localhost:8080/assets/logo.png" + }, + "languages": [], + "configuration": { + "urls": { + "streaming": "wss://localhost:8080" + }, + "accounts": { + "allow_custom_css": true, + "max_featured_tags": 10 + }, + "statuses": { + "max_characters": 5000, + "max_media_attachments": 6, + "characters_reserved_per_url": 25 + }, + "media_attachments": { + "supported_mime_types": [ + "image/jpeg", + "image/gif", + "image/png", + "image/webp", + "video/mp4" + ], + "image_size_limit": 10485760, + "image_matrix_limit": 16777216, + "video_size_limit": 41943040, + "video_frame_rate_limit": 60, + "video_matrix_limit": 16777216 + }, + "polls": { + "max_options": 6, + "max_characters_per_option": 50, + "min_expiration": 300, + "max_expiration": 2629746 + }, + "translation": { + "enabled": false + }, + "emojis": { + "emoji_size_limit": 51200 + } + }, + "registrations": { + "enabled": true, + "approval_required": true, + "message": null + }, + "contact": { + "email": "admin@example.org", + "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": "http://localhost:8080/assets/default_header.png", + "header_static": "http://localhost:8080/assets/default_header.png", + "followers_count": 1, + "following_count": 1, + "statuses_count": 4, + "last_status_at": "2021-10-20T10:41:37.000Z", + "emojis": [], + "fields": [], + "enable_rss": true, + "role": "admin" + } }, - "max_toot_chars": 0 + "rules": [] }`, string(b)) } |