From 268f252e0d517f2693b30d03fb8a68a0764a43bc Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Mon, 12 Sep 2022 13:03:23 +0200 Subject: [PATCH] [feature] Fetch + display custom emoji in statuses from remote instances (#807) * start implementing remote emoji fetcher * update status where pk * aaa * tidy up a little * check size limits for emojis * thank you linter, i love you <3 * update swagger docs * add emoji dereference test * make emoji max sizes configurable * normalize db.ErrAlreadyExists --- docs/api/swagger.yaml | 4 +- docs/configuration/media.md | 16 +++ example/config.yaml | 18 +++- internal/api/client/admin/emojicreate.go | 10 +- internal/config/config.go | 2 + internal/config/defaults.go | 2 + internal/config/flags.go | 2 + internal/config/helpers.gen.go | 50 +++++++++ internal/config/validate.go | 8 ++ internal/config/validate_test.go | 10 ++ internal/db/bundb/errors.go | 4 +- internal/db/bundb/status.go | 52 ++++++++++ internal/db/emoji.go | 2 + internal/db/error.go | 15 +-- internal/db/status.go | 3 + .../federation/dereferencing/dereferencer.go | 1 + internal/federation/dereferencing/emoji.go | 51 ++++++++++ .../federation/dereferencing/emoji_test.go | 95 ++++++++++++++++++ internal/federation/dereferencing/status.go | 76 +++++++++++--- internal/federation/federatingdb/create.go | 3 +- internal/gtsmodel/emoji.go | 2 +- internal/media/processingemoji.go | 6 ++ internal/processing/status/util.go | 3 +- test/cliparsing.sh | 22 ++-- test/envparsing.sh | 4 +- testrig/config.go | 2 + testrig/media/peglin.gif | Bin 0 -> 37796 bytes testrig/testmodels.go | 9 ++ 28 files changed, 424 insertions(+), 48 deletions(-) create mode 100644 internal/federation/dereferencing/emoji.go create mode 100644 internal/federation/dereferencing/emoji_test.go create mode 100644 testrig/media/peglin.gif diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml index 7ae1f5b54..ebcf14c02 100644 --- a/docs/api/swagger.yaml +++ b/docs/api/swagger.yaml @@ -2724,7 +2724,9 @@ paths: pattern: \w{2,30} required: true type: string - - description: A png or gif image of the emoji. Animated pngs work too! + - description: |- + A png or gif image of the emoji. Animated pngs work too! + To ensure compatibility with other fedi implementations, emoji size limit is 50kb by default. in: formData name: image required: true diff --git a/docs/configuration/media.md b/docs/configuration/media.md index 4adb3bedf..880a2bc95 100644 --- a/docs/configuration/media.md +++ b/docs/configuration/media.md @@ -39,4 +39,20 @@ media-description-max-chars: 500 # Examples: [30, 60, 7, 0] # Default: 30 media-remote-cache-days: 30 + +# Int. Max size in bytes of emojis uploaded to this instance via the admin API. +# The default is the same as the Mastodon size limit for emojis (50kb), which allows +# for good interoperability. Raising this limit may cause issues with federation +# of your emojis to other instances, so beware. +# Examples: [51200, 102400] +# Default: 51200 +media-emoji-local-max-size: 51200 + +# Int. Max size in bytes of emojis to download from other instances. +# By default this is 100kb, or twice the size of the default for media-emoji-local-max-size. +# This strikes a good balance between decent interoperability with instances that have +# higher emoji size limits, and not taking up too much space in storage. +# Examples: [51200, 102400] +# Default: 51200 +media-emoji-remote-max-size: 102400 ``` diff --git a/example/config.yaml b/example/config.yaml index 4655248e1..1998ce16a 100644 --- a/example/config.yaml +++ b/example/config.yaml @@ -211,7 +211,7 @@ accounts-reason-required: true ##### MEDIA CONFIG ##### ######################## -# Config pertaining to user media uploads (videos, image, image descriptions). +# Config pertaining to media uploads (videos, image, image descriptions, emoji). # Int. Maximum allowed image upload size in bytes. # Examples: [2097152, 10485760] @@ -244,6 +244,22 @@ media-description-max-chars: 500 # Default: 30 media-remote-cache-days: 30 +# Int. Max size in bytes of emojis uploaded to this instance via the admin API. +# The default is the same as the Mastodon size limit for emojis (50kb), which allows +# for good interoperability. Raising this limit may cause issues with federation +# of your emojis to other instances, so beware. +# Examples: [51200, 102400] +# Default: 51200 +media-emoji-local-max-size: 51200 + +# Int. Max size in bytes of emojis to download from other instances. +# By default this is 100kb, or twice the size of the default for media-emoji-local-max-size. +# This strikes a good balance between decent interoperability with instances that have +# higher emoji size limits, and not taking up too much space in storage. +# Examples: [51200, 102400] +# Default: 51200 +media-emoji-remote-max-size: 102400 + ########################## ##### STORAGE CONFIG ##### ########################## diff --git a/internal/api/client/admin/emojicreate.go b/internal/api/client/admin/emojicreate.go index 39ebd5adf..eef49b2c7 100644 --- a/internal/api/client/admin/emojicreate.go +++ b/internal/api/client/admin/emojicreate.go @@ -26,6 +26,7 @@ import ( "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/validate" @@ -56,7 +57,9 @@ import ( // required: true // - name: image // in: formData -// description: A png or gif image of the emoji. Animated pngs work too! +// description: |- +// A png or gif image of the emoji. Animated pngs work too! +// To ensure compatibility with other fedi implementations, emoji size limit is 50kb by default. // type: file // required: true // @@ -126,5 +129,10 @@ func validateCreateEmoji(form *model.EmojiCreateRequest) error { return errors.New("no emoji given") } + maxSize := config.GetMediaEmojiLocalMaxSize() + if form.Image.Size > int64(maxSize) { + return fmt.Errorf("emoji image too large: image is %dKB but size limit for custom emojis is %dKB", form.Image.Size/1024, maxSize/1024) + } + return validate.EmojiShortcode(form.Shortcode) } diff --git a/internal/config/config.go b/internal/config/config.go index d746bd12a..7efed1815 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -79,6 +79,8 @@ type Configuration struct { MediaDescriptionMinChars int `name:"media-description-min-chars" usage:"Min required chars for an image description"` MediaDescriptionMaxChars int `name:"media-description-max-chars" usage:"Max permitted chars for an image description"` MediaRemoteCacheDays int `name:"media-remote-cache-days" usage:"Number of days to locally cache media from remote instances. If set to 0, remote media will be kept indefinitely."` + MediaEmojiLocalMaxSize int `name:"media-emoji-local-max-size" usage:"Max size in bytes of emojis uploaded to this instance via the admin API."` + MediaEmojiRemoteMaxSize int `name:"media-emoji-remote-max-size" usage:"Max size in bytes of emojis to download from other instances."` StorageBackend string `name:"storage-backend" usage:"Storage backend to use for media attachments"` StorageLocalBasePath string `name:"storage-local-base-path" usage:"Full path to an already-created directory where gts should store/retrieve media files. Subfolders will be created within this dir."` diff --git a/internal/config/defaults.go b/internal/config/defaults.go index 48fd8f214..8a4a3129e 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -58,6 +58,8 @@ var Defaults = Configuration{ MediaDescriptionMinChars: 0, MediaDescriptionMaxChars: 500, MediaRemoteCacheDays: 30, + MediaEmojiLocalMaxSize: 51200, // 50kb + MediaEmojiRemoteMaxSize: 102400, // 100kb StorageBackend: "local", StorageLocalBasePath: "/gotosocial/storage", diff --git a/internal/config/flags.go b/internal/config/flags.go index 891449934..9b4c40428 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -75,6 +75,8 @@ func AddServerFlags(cmd *cobra.Command) { cmd.Flags().Int(MediaDescriptionMinCharsFlag(), cfg.MediaDescriptionMinChars, fieldtag("MediaDescriptionMinChars", "usage")) cmd.Flags().Int(MediaDescriptionMaxCharsFlag(), cfg.MediaDescriptionMaxChars, fieldtag("MediaDescriptionMaxChars", "usage")) cmd.Flags().Int(MediaRemoteCacheDaysFlag(), cfg.MediaRemoteCacheDays, fieldtag("MediaRemoteCacheDays", "usage")) + cmd.Flags().Int(MediaEmojiLocalMaxSizeFlag(), cfg.MediaEmojiLocalMaxSize, fieldtag("MediaEmojiLocalMaxSize", "usage")) + cmd.Flags().Int(MediaEmojiRemoteMaxSizeFlag(), cfg.MediaEmojiRemoteMaxSize, fieldtag("MediaEmojiRemoteMaxSize", "usage")) // Storage cmd.Flags().String(StorageBackendFlag(), cfg.StorageBackend, fieldtag("StorageBackend", "usage")) diff --git a/internal/config/helpers.gen.go b/internal/config/helpers.gen.go index a5dcc4c1c..51891a537 100644 --- a/internal/config/helpers.gen.go +++ b/internal/config/helpers.gen.go @@ -793,6 +793,56 @@ func GetMediaRemoteCacheDays() int { return global.GetMediaRemoteCacheDays() } // SetMediaRemoteCacheDays safely sets the value for global configuration 'MediaRemoteCacheDays' field func SetMediaRemoteCacheDays(v int) { global.SetMediaRemoteCacheDays(v) } +// GetMediaEmojiLocalMaxSize safely fetches the Configuration value for state's 'MediaEmojiLocalMaxSize' field +func (st *ConfigState) GetMediaEmojiLocalMaxSize() (v int) { + st.mutex.Lock() + v = st.config.MediaEmojiLocalMaxSize + st.mutex.Unlock() + return +} + +// SetMediaEmojiLocalMaxSize safely sets the Configuration value for state's 'MediaEmojiLocalMaxSize' field +func (st *ConfigState) SetMediaEmojiLocalMaxSize(v int) { + st.mutex.Lock() + defer st.mutex.Unlock() + st.config.MediaEmojiLocalMaxSize = v + st.reloadToViper() +} + +// MediaEmojiLocalMaxSizeFlag returns the flag name for the 'MediaEmojiLocalMaxSize' field +func MediaEmojiLocalMaxSizeFlag() string { return "media-emoji-local-max-size" } + +// GetMediaEmojiLocalMaxSize safely fetches the value for global configuration 'MediaEmojiLocalMaxSize' field +func GetMediaEmojiLocalMaxSize() int { return global.GetMediaEmojiLocalMaxSize() } + +// SetMediaEmojiLocalMaxSize safely sets the value for global configuration 'MediaEmojiLocalMaxSize' field +func SetMediaEmojiLocalMaxSize(v int) { global.SetMediaEmojiLocalMaxSize(v) } + +// GetMediaEmojiRemoteMaxSize safely fetches the Configuration value for state's 'MediaEmojiRemoteMaxSize' field +func (st *ConfigState) GetMediaEmojiRemoteMaxSize() (v int) { + st.mutex.Lock() + v = st.config.MediaEmojiRemoteMaxSize + st.mutex.Unlock() + return +} + +// SetMediaEmojiRemoteMaxSize safely sets the Configuration value for state's 'MediaEmojiRemoteMaxSize' field +func (st *ConfigState) SetMediaEmojiRemoteMaxSize(v int) { + st.mutex.Lock() + defer st.mutex.Unlock() + st.config.MediaEmojiRemoteMaxSize = v + st.reloadToViper() +} + +// MediaEmojiRemoteMaxSizeFlag returns the flag name for the 'MediaEmojiRemoteMaxSize' field +func MediaEmojiRemoteMaxSizeFlag() string { return "media-emoji-remote-max-size" } + +// GetMediaEmojiRemoteMaxSize safely fetches the value for global configuration 'MediaEmojiRemoteMaxSize' field +func GetMediaEmojiRemoteMaxSize() int { return global.GetMediaEmojiRemoteMaxSize() } + +// SetMediaEmojiRemoteMaxSize safely sets the value for global configuration 'MediaEmojiRemoteMaxSize' field +func SetMediaEmojiRemoteMaxSize(v int) { global.SetMediaEmojiRemoteMaxSize(v) } + // GetStorageBackend safely fetches the Configuration value for state's 'StorageBackend' field func (st *ConfigState) GetStorageBackend() (v string) { st.mutex.Lock() diff --git a/internal/config/validate.go b/internal/config/validate.go index 064eae07a..b9fdb013b 100644 --- a/internal/config/validate.go +++ b/internal/config/validate.go @@ -67,6 +67,14 @@ func Validate() error { errs = append(errs, fmt.Errorf("%s must be set", WebAssetBaseDirFlag())) } + if m := GetMediaEmojiLocalMaxSize(); m < 0 { + errs = append(errs, fmt.Errorf("%s must not be less than 0", MediaEmojiLocalMaxSizeFlag())) + } + + if m := GetMediaEmojiRemoteMaxSize(); m < 0 { + errs = append(errs, fmt.Errorf("%s must not be less than 0", MediaEmojiRemoteMaxSizeFlag())) + } + if len(errs) > 0 { errStrings := []string{} for _, err := range errs { diff --git a/internal/config/validate_test.go b/internal/config/validate_test.go index c3a998a4a..f7450cdaa 100644 --- a/internal/config/validate_test.go +++ b/internal/config/validate_test.go @@ -141,6 +141,16 @@ func (suite *ConfigValidateTestSuite) TestValidateConfigBadProtocolNoHost() { suite.EqualError(err, "host must be set; protocol must be set to either http or https, provided value was foo") } +func (suite *ConfigValidateTestSuite) TestValidateConfigBadEmojiSizes() { + testrig.InitTestConfig() + + config.SetMediaEmojiLocalMaxSize(-10) + config.SetMediaEmojiRemoteMaxSize(-50) + + err := config.Validate() + suite.EqualError(err, "media-emoji-local-max-size must not be less than 0; media-emoji-remote-max-size must not be less than 0") +} + func TestConfigValidateTestSuite(t *testing.T) { suite.Run(t, &ConfigValidateTestSuite{}) } diff --git a/internal/db/bundb/errors.go b/internal/db/bundb/errors.go index 67a673e15..7d0157373 100644 --- a/internal/db/bundb/errors.go +++ b/internal/db/bundb/errors.go @@ -19,7 +19,7 @@ func processPostgresError(err error) db.Error { // (https://www.postgresql.org/docs/10/errcodes-appendix.html) switch pgErr.Code { case "23505" /* unique_violation */ : - return db.NewErrAlreadyExists(pgErr.Message) + return db.ErrAlreadyExists default: return err } @@ -36,7 +36,7 @@ func processSQLiteError(err error) db.Error { // Handle supplied error code: switch sqliteErr.Code() { case sqlite3.SQLITE_CONSTRAINT_UNIQUE, sqlite3.SQLITE_CONSTRAINT_PRIMARYKEY: - return db.NewErrAlreadyExists(err.Error()) + return db.ErrAlreadyExists default: return err } diff --git a/internal/db/bundb/status.go b/internal/db/bundb/status.go index 378ee1a7a..e247e8940 100644 --- a/internal/db/bundb/status.go +++ b/internal/db/bundb/status.go @@ -22,6 +22,7 @@ import ( "container/list" "context" "database/sql" + "errors" "time" "github.com/superseriousbusiness/gotosocial/internal/cache" @@ -175,6 +176,57 @@ func (s *statusDB) PutStatus(ctx context.Context, status *gtsmodel.Status) db.Er }) } +func (s *statusDB) UpdateStatus(ctx context.Context, status *gtsmodel.Status) (*gtsmodel.Status, db.Error) { + err := s.conn.RunInTx(ctx, func(tx bun.Tx) error { + // create links between this status and any emojis it uses + for _, i := range status.EmojiIDs { + if _, err := tx.NewInsert().Model(>smodel.StatusToEmoji{ + StatusID: status.ID, + EmojiID: i, + }).Exec(ctx); err != nil { + err = s.conn.errProc(err) + if !errors.Is(err, db.ErrAlreadyExists) { + return err + } + } + } + + // create links between this status and any tags it uses + for _, i := range status.TagIDs { + if _, err := tx.NewInsert().Model(>smodel.StatusToTag{ + StatusID: status.ID, + TagID: i, + }).Exec(ctx); err != nil { + err = s.conn.errProc(err) + if !errors.Is(err, db.ErrAlreadyExists) { + return err + } + } + } + + // change the status ID of the media attachments to this status + for _, a := range status.Attachments { + a.StatusID = status.ID + a.UpdatedAt = time.Now() + if _, err := tx.NewUpdate().Model(a). + Where("id = ?", a.ID). + Exec(ctx); err != nil { + return err + } + } + + // Finally, update the status itself + if _, err := tx.NewUpdate().Model(status).WherePK().Exec(ctx); err != nil { + return err + } + + s.cache.Put(status) + return nil + }) + + return status, err +} + func (s *statusDB) GetStatusParents(ctx context.Context, status *gtsmodel.Status, onlyDirect bool) ([]*gtsmodel.Status, db.Error) { parents := []*gtsmodel.Status{} s.statusParent(ctx, status, &parents, onlyDirect) diff --git a/internal/db/emoji.go b/internal/db/emoji.go index 0038e10e4..374fd7b12 100644 --- a/internal/db/emoji.go +++ b/internal/db/emoji.go @@ -35,4 +35,6 @@ type Emoji interface { // GetEmojiByShortcodeDomain gets an emoji based on its shortcode and domain. // For local emoji, domain should be an empty string. GetEmojiByShortcodeDomain(ctx context.Context, shortcode string, domain string) (*gtsmodel.Emoji, Error) + // GetEmojiByURI returns one emoji based on its ActivityPub URI. + GetEmojiByURI(ctx context.Context, uri string) (*gtsmodel.Emoji, Error) } diff --git a/internal/db/error.go b/internal/db/error.go index 9ac0b6aa0..8dc344360 100644 --- a/internal/db/error.go +++ b/internal/db/error.go @@ -28,19 +28,8 @@ var ( ErrNoEntries Error = fmt.Errorf("no entries") // ErrMultipleEntries is returned when a caller expected ONE entry for a query, but multiples were found. ErrMultipleEntries Error = fmt.Errorf("multiple entries") + // ErrAlreadyExists is returned when a conflict was encountered in the db when doing an insert. + ErrAlreadyExists Error = fmt.Errorf("already exists") // ErrUnknown denotes an unknown database error. ErrUnknown Error = fmt.Errorf("unknown error") ) - -// ErrAlreadyExists is returned when a caller tries to insert a database entry that already exists in the db. -type ErrAlreadyExists struct { - message string -} - -func (e *ErrAlreadyExists) Error() string { - return e.message -} - -func NewErrAlreadyExists(msg string) error { - return &ErrAlreadyExists{message: msg} -} diff --git a/internal/db/status.go b/internal/db/status.go index 74eb0d4ff..307d9ea74 100644 --- a/internal/db/status.go +++ b/internal/db/status.go @@ -38,6 +38,9 @@ type Status interface { // PutStatus stores one status in the database. PutStatus(ctx context.Context, status *gtsmodel.Status) Error + // UpdateStatus updates one status in the database and returns it to the caller. + UpdateStatus(ctx context.Context, status *gtsmodel.Status) (*gtsmodel.Status, Error) + // CountStatusReplies returns the amount of replies recorded for a status, or an error if something goes wrong CountStatusReplies(ctx context.Context, status *gtsmodel.Status) (int, Error) diff --git a/internal/federation/dereferencing/dereferencer.go b/internal/federation/dereferencing/dereferencer.go index 4f7559be3..0fad2405e 100644 --- a/internal/federation/dereferencing/dereferencer.go +++ b/internal/federation/dereferencing/dereferencer.go @@ -41,6 +41,7 @@ type Dereferencer interface { GetRemoteInstance(ctx context.Context, username string, remoteInstanceURI *url.URL) (*gtsmodel.Instance, error) GetRemoteMedia(ctx context.Context, requestingUsername string, accountID string, remoteURL string, ai *media.AdditionalMediaInfo) (*media.ProcessingMedia, error) + GetRemoteEmoji(ctx context.Context, requestingUsername string, remoteURL string, shortcode string, id string, emojiURI string, ai *media.AdditionalEmojiInfo) (*media.ProcessingEmoji, error) DereferenceAnnounce(ctx context.Context, announce *gtsmodel.Status, requestingUsername string) error DereferenceThread(ctx context.Context, username string, statusIRI *url.URL) error diff --git a/internal/federation/dereferencing/emoji.go b/internal/federation/dereferencing/emoji.go new file mode 100644 index 000000000..49811b131 --- /dev/null +++ b/internal/federation/dereferencing/emoji.go @@ -0,0 +1,51 @@ +/* + 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 . +*/ + +package dereferencing + +import ( + "context" + "fmt" + "io" + "net/url" + + "github.com/superseriousbusiness/gotosocial/internal/media" +) + +func (d *deref) GetRemoteEmoji(ctx context.Context, requestingUsername string, remoteURL string, shortcode string, id string, emojiURI string, ai *media.AdditionalEmojiInfo) (*media.ProcessingEmoji, error) { + t, err := d.transportController.NewTransportForUsername(ctx, requestingUsername) + if err != nil { + return nil, fmt.Errorf("GetRemoteEmoji: error creating transport: %s", err) + } + + derefURI, err := url.Parse(remoteURL) + if err != nil { + return nil, fmt.Errorf("GetRemoteEmoji: error parsing url: %s", err) + } + + dataFunc := func(innerCtx context.Context) (io.Reader, int, error) { + return t.DereferenceMedia(innerCtx, derefURI) + } + + processingMedia, err := d.mediaManager.ProcessEmoji(ctx, dataFunc, nil, shortcode, id, emojiURI, ai) + if err != nil { + return nil, fmt.Errorf("GetRemoteEmoji: error processing emoji: %s", err) + } + + return processingMedia, nil +} diff --git a/internal/federation/dereferencing/emoji_test.go b/internal/federation/dereferencing/emoji_test.go new file mode 100644 index 000000000..b03d839ce --- /dev/null +++ b/internal/federation/dereferencing/emoji_test.go @@ -0,0 +1,95 @@ +/* + 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 . +*/ + +package dereferencing_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/media" +) + +type EmojiTestSuite struct { + DereferencerStandardTestSuite +} + +func (suite *EmojiTestSuite) TestDereferenceEmojiBlocking() { + ctx := context.Background() + fetchingAccount := suite.testAccounts["local_account_1"] + emojiImageRemoteURL := "http://example.org/media/emojis/1781772.gif" + emojiImageStaticRemoteURL := "http://example.org/media/emojis/1781772.gif" + emojiURI := "http://example.org/emojis/1781772" + emojiShortcode := "peglin" + emojiID := "01GCBMGNZBKMEE1KTZ6PMJEW5D" + emojiDomain := "example.org" + emojiDisabled := false + emojiVisibleInPicker := false + + ai := &media.AdditionalEmojiInfo{ + Domain: &emojiDomain, + ImageRemoteURL: &emojiImageRemoteURL, + ImageStaticRemoteURL: &emojiImageStaticRemoteURL, + Disabled: &emojiDisabled, + VisibleInPicker: &emojiVisibleInPicker, + } + + processingEmoji, err := suite.dereferencer.GetRemoteEmoji(ctx, fetchingAccount.Username, emojiImageRemoteURL, emojiShortcode, emojiID, emojiURI, ai) + suite.NoError(err) + + // make a blocking call to load the emoji from the in-process media + emoji, err := processingEmoji.LoadEmoji(ctx) + suite.NoError(err) + suite.NotNil(emoji) + + suite.Equal(emojiID, emoji.ID) + suite.WithinDuration(time.Now(), emoji.CreatedAt, 10*time.Second) + suite.WithinDuration(time.Now(), emoji.UpdatedAt, 10*time.Second) + suite.Equal(emojiShortcode, emoji.Shortcode) + suite.Equal(emojiDomain, emoji.Domain) + suite.Equal(emojiImageRemoteURL, emoji.ImageRemoteURL) + suite.Equal(emojiImageStaticRemoteURL, emoji.ImageStaticRemoteURL) + suite.Contains(emoji.ImageURL, "/emoji/original/01GCBMGNZBKMEE1KTZ6PMJEW5D.gif") + suite.Contains(emoji.ImageStaticURL, "emoji/static/01GCBMGNZBKMEE1KTZ6PMJEW5D.png") + suite.Contains(emoji.ImagePath, "/emoji/original/01GCBMGNZBKMEE1KTZ6PMJEW5D.gif") + suite.Contains(emoji.ImageStaticPath, "/emoji/static/01GCBMGNZBKMEE1KTZ6PMJEW5D.png") + suite.Equal("image/gif", emoji.ImageContentType) + suite.Equal("image/png", emoji.ImageStaticContentType) + suite.Equal(37796, emoji.ImageFileSize) + suite.Equal(7951, emoji.ImageStaticFileSize) + suite.WithinDuration(time.Now(), emoji.ImageUpdatedAt, 10*time.Second) + suite.False(*emoji.Disabled) + suite.Equal(emojiURI, emoji.URI) + suite.False(*emoji.VisibleInPicker) + suite.Empty(emoji.CategoryID) + + // ensure that emoji is now in storage + stored, err := suite.storage.Get(ctx, emoji.ImagePath) + suite.NoError(err) + suite.Len(stored, emoji.ImageFileSize) + + storedStatic, err := suite.storage.Get(ctx, emoji.ImageStaticPath) + suite.NoError(err) + suite.Len(storedStatic, emoji.ImageStaticFileSize) +} + +func TestEmojiTestSuite(t *testing.T) { + suite.Run(t, new(EmojiTestSuite)) +} diff --git a/internal/federation/dereferencing/status.go b/internal/federation/dereferencing/status.go index e6e03646c..f3b7ee96e 100644 --- a/internal/federation/dereferencing/status.go +++ b/internal/federation/dereferencing/status.go @@ -26,10 +26,10 @@ import ( "net/url" "strings" - "codeberg.org/gruf/go-kv" "github.com/superseriousbusiness/activity/streams" "github.com/superseriousbusiness/activity/streams/vocab" "github.com/superseriousbusiness/gotosocial/internal/ap" + "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/log" @@ -46,11 +46,7 @@ func (d *deref) EnrichRemoteStatus(ctx context.Context, username string, status return nil, err } - if err := d.db.UpdateByPrimaryKey(ctx, status); err != nil { - return nil, fmt.Errorf("EnrichRemoteStatus: error updating status: %s", err) - } - - return status, nil + return d.db.UpdateStatus(ctx, status) } // GetRemoteStatus completely dereferences a remote status, converts it to a GtS model status, @@ -225,12 +221,6 @@ func (d *deref) dereferenceStatusable(ctx context.Context, username string, remo // and attach them to the status. The status itself will not be added to the database yet, // that's up the caller to do. func (d *deref) populateStatusFields(ctx context.Context, status *gtsmodel.Status, requestingUsername string, includeParent bool) error { - l := log.WithFields(kv.Fields{ - - {"status", status}, - }...) - l.Debug("entering function") - statusIRI, err := url.Parse(status.URI) if err != nil { return fmt.Errorf("populateStatusFields: couldn't parse status URI %s: %s", status.URI, err) @@ -262,7 +252,9 @@ func (d *deref) populateStatusFields(ctx context.Context, status *gtsmodel.Statu // TODO // 3. Emojis - // TODO + if err := d.populateStatusEmojis(ctx, status, requestingUsername); err != nil { + return fmt.Errorf("populateStatusFields: error populating status emojis: %s", err) + } // 4. Mentions // TODO: do we need to handle removing empty mention objects and just using mention IDs slice? @@ -413,6 +405,64 @@ func (d *deref) populateStatusAttachments(ctx context.Context, status *gtsmodel. return nil } +func (d *deref) populateStatusEmojis(ctx context.Context, status *gtsmodel.Status, requestingUsername string) error { + // At this point we should know: + // * the AP uri of the emoji + // * the domain of the emoji + // * the shortcode of the emoji + // * the remote URL of the image + // This should be enough to dereference the emoji + + gotEmojis := make([]*gtsmodel.Emoji, 0, len(status.Emojis)) + emojiIDs := make([]string, 0, len(status.Emojis)) + + for _, e := range status.Emojis { + var gotEmoji *gtsmodel.Emoji + var err error + + // check if we've already got this emoji in the db + if gotEmoji, err = d.db.GetEmojiByURI(ctx, e.URI); err != nil && err != db.ErrNoEntries { + log.Errorf("populateStatusEmojis: error checking database for emoji %s: %s", e.URI, err) + continue + } + + if gotEmoji == nil { + // it's new! go get it! + newEmojiID, err := id.NewRandomULID() + if err != nil { + log.Errorf("populateStatusEmojis: error generating id for remote emoji %s: %s", e.URI, err) + continue + } + + processingEmoji, err := d.GetRemoteEmoji(ctx, requestingUsername, e.ImageRemoteURL, e.Shortcode, newEmojiID, e.URI, &media.AdditionalEmojiInfo{ + Domain: &e.Domain, + ImageRemoteURL: &e.ImageRemoteURL, + ImageStaticRemoteURL: &e.ImageRemoteURL, + Disabled: e.Disabled, + VisibleInPicker: e.VisibleInPicker, + }) + + if err != nil { + log.Errorf("populateStatusEmojis: couldn't get remote emoji %s: %s", e.URI, err) + continue + } + + if gotEmoji, err = processingEmoji.LoadEmoji(ctx); err != nil { + log.Errorf("populateStatusEmojis: couldn't load remote emoji %s: %s", e.URI, err) + continue + } + } + + // if we get here, we either had the emoji already or we successfully fetched it + gotEmojis = append(gotEmojis, gotEmoji) + emojiIDs = append(emojiIDs, gotEmoji.ID) + } + + status.Emojis = gotEmojis + status.EmojiIDs = emojiIDs + return nil +} + func (d *deref) populateStatusRepliedTo(ctx context.Context, status *gtsmodel.Status, requestingUsername string) error { if status.InReplyToURI != "" && status.InReplyToID == "" { statusURI, err := url.Parse(status.InReplyToURI) diff --git a/internal/federation/federatingdb/create.go b/internal/federation/federatingdb/create.go index a6e55f2ad..25e961bc3 100644 --- a/internal/federation/federatingdb/create.go +++ b/internal/federation/federatingdb/create.go @@ -226,8 +226,7 @@ func (f *federatingDB) createNote(ctx context.Context, note vocab.ActivityStream status.ID = statusID if err := f.db.PutStatus(ctx, status); err != nil { - var alreadyExistsError *db.ErrAlreadyExists - if errors.As(err, &alreadyExistsError) { + if errors.Is(err, db.ErrAlreadyExists) { // the status already exists in the database, which means we've already handled everything else, // so we can just return nil here and be done with it. return nil diff --git a/internal/gtsmodel/emoji.go b/internal/gtsmodel/emoji.go index 106301041..2cc72a762 100644 --- a/internal/gtsmodel/emoji.go +++ b/internal/gtsmodel/emoji.go @@ -20,7 +20,7 @@ package gtsmodel import "time" -// Emoji represents a custom emoji that's been uploaded through the admin UI, and is useable by instance denizens. +// Emoji represents a custom emoji that's been uploaded through the admin UI or downloaded from a remote instance. type Emoji struct { ID string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database CreatedAt time.Time `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created diff --git a/internal/media/processingemoji.go b/internal/media/processingemoji.go index 3b3023f2a..121f54ddc 100644 --- a/internal/media/processingemoji.go +++ b/internal/media/processingemoji.go @@ -28,6 +28,7 @@ import ( "sync/atomic" "time" + "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" @@ -170,6 +171,11 @@ func (p *ProcessingEmoji) store(ctx context.Context) error { return fmt.Errorf("store: error executing data function: %s", err) } + maxSize := config.GetMediaEmojiRemoteMaxSize() + if fileSize > maxSize { + return fmt.Errorf("store: emoji size (%db) is larger than allowed emojiRemoteMaxSize (%db)", fileSize, maxSize) + } + // defer closing the reader when we're done with it defer func() { if rc, ok := reader.(io.ReadCloser); ok { diff --git a/internal/processing/status/util.go b/internal/processing/status/util.go index 880de1db3..298d4fbd0 100644 --- a/internal/processing/status/util.go +++ b/internal/processing/status/util.go @@ -234,8 +234,7 @@ func (p *processor) ProcessTags(ctx context.Context, form *apimodel.AdvancedStat } for _, tag := range gtsTags { if err := p.db.Put(ctx, tag); err != nil { - var alreadyExistsError *db.ErrAlreadyExists - if !errors.As(err, &alreadyExistsError) { + if !errors.Is(err, db.ErrAlreadyExists) { return fmt.Errorf("error putting tags in db: %s", err) } } diff --git a/test/cliparsing.sh b/test/cliparsing.sh index 1d6f2e947..c1a30e693 100755 --- a/test/cliparsing.sh +++ b/test/cliparsing.sh @@ -5,7 +5,7 @@ set -e echo "STARTING CLI TESTS" echo "TEST_1 Make sure defaults are set correctly." -TEST_1_EXPECTED='{"account-domain":"","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"","db-address":"","db-database":"gotosocial","db-password":"","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"","email":"","host":"","instance-expose-peers":false,"instance-expose-suspended":false,"letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":false,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":10485760,"media-remote-cache-days":30,"media-video-max-size":41943040,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","profile","email","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"GoToSocial","smtp-host":"","smtp-password":"","smtp-port":0,"smtp-username":"","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","storage-s3-access-key":"","storage-s3-bucket":"","storage-s3-endpoint":"","storage-s3-secret-key":"","storage-s3-use-ssl":true,"syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' +TEST_1_EXPECTED='{"account-domain":"","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"","db-address":"","db-database":"gotosocial","db-password":"","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"","email":"","host":"","instance-expose-peers":false,"instance-expose-suspended":false,"letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":false,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-emoji-local-max-size":51200,"media-emoji-remote-max-size":102400,"media-image-max-size":10485760,"media-remote-cache-days":30,"media-video-max-size":41943040,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","profile","email","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"GoToSocial","smtp-host":"","smtp-password":"","smtp-port":0,"smtp-username":"","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","storage-s3-access-key":"","storage-s3-bucket":"","storage-s3-endpoint":"","storage-s3-secret-key":"","storage-s3-use-ssl":true,"syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' TEST_1="$(go run ./cmd/gotosocial/... debug config)" if [ "${TEST_1}" != "${TEST_1_EXPECTED}" ]; then echo "TEST_1 not equal TEST_1_EXPECTED" @@ -15,7 +15,7 @@ else fi echo "TEST_2 Override db-address from default using cli flag." -TEST_2_EXPECTED='{"account-domain":"","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"","db-address":"some.db.address","db-database":"gotosocial","db-password":"","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"","email":"","host":"","instance-expose-peers":false,"instance-expose-suspended":false,"letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":false,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":10485760,"media-remote-cache-days":30,"media-video-max-size":41943040,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","profile","email","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"GoToSocial","smtp-host":"","smtp-password":"","smtp-port":0,"smtp-username":"","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","storage-s3-access-key":"","storage-s3-bucket":"","storage-s3-endpoint":"","storage-s3-secret-key":"","storage-s3-use-ssl":true,"syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' +TEST_2_EXPECTED='{"account-domain":"","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"","db-address":"some.db.address","db-database":"gotosocial","db-password":"","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"","email":"","host":"","instance-expose-peers":false,"instance-expose-suspended":false,"letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":false,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-emoji-local-max-size":51200,"media-emoji-remote-max-size":102400,"media-image-max-size":10485760,"media-remote-cache-days":30,"media-video-max-size":41943040,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","profile","email","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"GoToSocial","smtp-host":"","smtp-password":"","smtp-port":0,"smtp-username":"","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","storage-s3-access-key":"","storage-s3-bucket":"","storage-s3-endpoint":"","storage-s3-secret-key":"","storage-s3-use-ssl":true,"syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' TEST_2="$(go run ./cmd/gotosocial/... --db-address some.db.address debug config)" if [ "${TEST_2}" != "${TEST_2_EXPECTED}" ]; then echo "TEST_2 not equal TEST_2_EXPECTED" @@ -25,7 +25,7 @@ else fi echo "TEST_3 Override db-address from default using env var." -TEST_3_EXPECTED='{"account-domain":"","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"","db-address":"some.db.address","db-database":"gotosocial","db-password":"","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"","email":"","host":"","instance-expose-peers":false,"instance-expose-suspended":false,"letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":false,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":10485760,"media-remote-cache-days":30,"media-video-max-size":41943040,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","profile","email","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"GoToSocial","smtp-host":"","smtp-password":"","smtp-port":0,"smtp-username":"","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","storage-s3-access-key":"","storage-s3-bucket":"","storage-s3-endpoint":"","storage-s3-secret-key":"","storage-s3-use-ssl":true,"syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' +TEST_3_EXPECTED='{"account-domain":"","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"","db-address":"some.db.address","db-database":"gotosocial","db-password":"","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"","email":"","host":"","instance-expose-peers":false,"instance-expose-suspended":false,"letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":false,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-emoji-local-max-size":51200,"media-emoji-remote-max-size":102400,"media-image-max-size":10485760,"media-remote-cache-days":30,"media-video-max-size":41943040,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","profile","email","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"GoToSocial","smtp-host":"","smtp-password":"","smtp-port":0,"smtp-username":"","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","storage-s3-access-key":"","storage-s3-bucket":"","storage-s3-endpoint":"","storage-s3-secret-key":"","storage-s3-use-ssl":true,"syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' TEST_3="$(GTS_DB_ADDRESS=some.db.address go run ./cmd/gotosocial/... debug config)" if [ "${TEST_3}" != "${TEST_3_EXPECTED}" ]; then echo "TEST_3 not equal TEST_3_EXPECTED" @@ -35,7 +35,7 @@ else fi echo "TEST_4 Override db-address from default using both env var and cli flag. The cli flag should take priority." -TEST_4_EXPECTED='{"account-domain":"","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"","db-address":"some.other.db.address","db-database":"gotosocial","db-password":"","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"","email":"","host":"","instance-expose-peers":false,"instance-expose-suspended":false,"letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":false,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":10485760,"media-remote-cache-days":30,"media-video-max-size":41943040,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","profile","email","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"GoToSocial","smtp-host":"","smtp-password":"","smtp-port":0,"smtp-username":"","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","storage-s3-access-key":"","storage-s3-bucket":"","storage-s3-endpoint":"","storage-s3-secret-key":"","storage-s3-use-ssl":true,"syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' +TEST_4_EXPECTED='{"account-domain":"","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"","db-address":"some.other.db.address","db-database":"gotosocial","db-password":"","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"","email":"","host":"","instance-expose-peers":false,"instance-expose-suspended":false,"letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":false,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-emoji-local-max-size":51200,"media-emoji-remote-max-size":102400,"media-image-max-size":10485760,"media-remote-cache-days":30,"media-video-max-size":41943040,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","profile","email","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"GoToSocial","smtp-host":"","smtp-password":"","smtp-port":0,"smtp-username":"","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","storage-s3-access-key":"","storage-s3-bucket":"","storage-s3-endpoint":"","storage-s3-secret-key":"","storage-s3-use-ssl":true,"syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' TEST_4="$(GTS_DB_ADDRESS=some.db.address go run ./cmd/gotosocial/... --db-address some.other.db.address debug config)" if [ "${TEST_4}" != "${TEST_4_EXPECTED}" ]; then echo "TEST_4 not equal TEST_4_EXPECTED" @@ -45,7 +45,7 @@ else fi echo "TEST_5 Test loading a config file by passing an env var." -TEST_5_EXPECTED='{"account-domain":"example.org","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test.yaml","db-address":"127.0.0.1","db-database":"postgres","db-password":"postgres","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"postgres","email":"","host":"gts.example.org","instance-expose-peers":false,"instance-expose-suspended":false,"letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":10485760,"media-remote-cache-days":30,"media-video-max-size":41943040,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","email","profile","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"someone@example.org","smtp-host":"verycoolemailhost.mail","smtp-password":"smtp-password","smtp-port":8888,"smtp-username":"smtp-username","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","storage-s3-access-key":"","storage-s3-bucket":"","storage-s3-endpoint":"","storage-s3-secret-key":"","storage-s3-use-ssl":true,"syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","0.0.0.0/0"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' +TEST_5_EXPECTED='{"account-domain":"example.org","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test.yaml","db-address":"127.0.0.1","db-database":"postgres","db-password":"postgres","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"postgres","email":"","host":"gts.example.org","instance-expose-peers":false,"instance-expose-suspended":false,"letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-emoji-local-max-size":51200,"media-emoji-remote-max-size":102400,"media-image-max-size":10485760,"media-remote-cache-days":30,"media-video-max-size":41943040,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","email","profile","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"someone@example.org","smtp-host":"verycoolemailhost.mail","smtp-password":"smtp-password","smtp-port":8888,"smtp-username":"smtp-username","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","storage-s3-access-key":"","storage-s3-bucket":"","storage-s3-endpoint":"","storage-s3-secret-key":"","storage-s3-use-ssl":true,"syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","0.0.0.0/0"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' TEST_5="$(GTS_CONFIG_PATH=./test/test.yaml go run ./cmd/gotosocial/... debug config)" if [ "${TEST_5}" != "${TEST_5_EXPECTED}" ]; then echo "TEST_5 not equal TEST_5_EXPECTED" @@ -55,7 +55,7 @@ else fi echo "TEST_6 Test loading a config file by passing cli flag." -TEST_6_EXPECTED='{"account-domain":"example.org","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test.yaml","db-address":"127.0.0.1","db-database":"postgres","db-password":"postgres","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"postgres","email":"","host":"gts.example.org","instance-expose-peers":false,"instance-expose-suspended":false,"letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":10485760,"media-remote-cache-days":30,"media-video-max-size":41943040,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","email","profile","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"someone@example.org","smtp-host":"verycoolemailhost.mail","smtp-password":"smtp-password","smtp-port":8888,"smtp-username":"smtp-username","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","storage-s3-access-key":"","storage-s3-bucket":"","storage-s3-endpoint":"","storage-s3-secret-key":"","storage-s3-use-ssl":true,"syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","0.0.0.0/0"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' +TEST_6_EXPECTED='{"account-domain":"example.org","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test.yaml","db-address":"127.0.0.1","db-database":"postgres","db-password":"postgres","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"postgres","email":"","host":"gts.example.org","instance-expose-peers":false,"instance-expose-suspended":false,"letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-emoji-local-max-size":51200,"media-emoji-remote-max-size":102400,"media-image-max-size":10485760,"media-remote-cache-days":30,"media-video-max-size":41943040,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","email","profile","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"someone@example.org","smtp-host":"verycoolemailhost.mail","smtp-password":"smtp-password","smtp-port":8888,"smtp-username":"smtp-username","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","storage-s3-access-key":"","storage-s3-bucket":"","storage-s3-endpoint":"","storage-s3-secret-key":"","storage-s3-use-ssl":true,"syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","0.0.0.0/0"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' TEST_6="$(go run ./cmd/gotosocial/... --config-path ./test/test.yaml debug config)" if [ "${TEST_6}" != "${TEST_6_EXPECTED}" ]; then echo "TEST_6 not equal TEST_6_EXPECTED" @@ -65,7 +65,7 @@ else fi echo "TEST_7 Test loading a config file and overriding one of the variables with a cli flag." -TEST_7_EXPECTED='{"account-domain":"","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test.yaml","db-address":"127.0.0.1","db-database":"postgres","db-password":"postgres","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"postgres","email":"","host":"gts.example.org","instance-expose-peers":false,"instance-expose-suspended":false,"letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":10485760,"media-remote-cache-days":30,"media-video-max-size":41943040,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","email","profile","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"someone@example.org","smtp-host":"verycoolemailhost.mail","smtp-password":"smtp-password","smtp-port":8888,"smtp-username":"smtp-username","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","storage-s3-access-key":"","storage-s3-bucket":"","storage-s3-endpoint":"","storage-s3-secret-key":"","storage-s3-use-ssl":true,"syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","0.0.0.0/0"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' +TEST_7_EXPECTED='{"account-domain":"","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test.yaml","db-address":"127.0.0.1","db-database":"postgres","db-password":"postgres","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"postgres","email":"","host":"gts.example.org","instance-expose-peers":false,"instance-expose-suspended":false,"letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-emoji-local-max-size":51200,"media-emoji-remote-max-size":102400,"media-image-max-size":10485760,"media-remote-cache-days":30,"media-video-max-size":41943040,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","email","profile","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"someone@example.org","smtp-host":"verycoolemailhost.mail","smtp-password":"smtp-password","smtp-port":8888,"smtp-username":"smtp-username","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","storage-s3-access-key":"","storage-s3-bucket":"","storage-s3-endpoint":"","storage-s3-secret-key":"","storage-s3-use-ssl":true,"syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","0.0.0.0/0"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' TEST_7="$(go run ./cmd/gotosocial/... --config-path ./test/test.yaml --account-domain '' debug config)" if [ "${TEST_7}" != "${TEST_7_EXPECTED}" ]; then echo "TEST_7 not equal TEST_7_EXPECTED" @@ -75,7 +75,7 @@ else fi echo "TEST_8 Test loading a config file and overriding one of the variables with an env var." -TEST_8_EXPECTED='{"account-domain":"peepee","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test.yaml","db-address":"127.0.0.1","db-database":"postgres","db-password":"postgres","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"postgres","email":"","host":"gts.example.org","instance-expose-peers":false,"instance-expose-suspended":false,"letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":10485760,"media-remote-cache-days":30,"media-video-max-size":41943040,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","email","profile","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"someone@example.org","smtp-host":"verycoolemailhost.mail","smtp-password":"smtp-password","smtp-port":8888,"smtp-username":"smtp-username","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","storage-s3-access-key":"","storage-s3-bucket":"","storage-s3-endpoint":"","storage-s3-secret-key":"","storage-s3-use-ssl":true,"syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","0.0.0.0/0"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' +TEST_8_EXPECTED='{"account-domain":"peepee","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test.yaml","db-address":"127.0.0.1","db-database":"postgres","db-password":"postgres","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"postgres","email":"","host":"gts.example.org","instance-expose-peers":false,"instance-expose-suspended":false,"letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-emoji-local-max-size":51200,"media-emoji-remote-max-size":102400,"media-image-max-size":10485760,"media-remote-cache-days":30,"media-video-max-size":41943040,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","email","profile","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"someone@example.org","smtp-host":"verycoolemailhost.mail","smtp-password":"smtp-password","smtp-port":8888,"smtp-username":"smtp-username","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","storage-s3-access-key":"","storage-s3-bucket":"","storage-s3-endpoint":"","storage-s3-secret-key":"","storage-s3-use-ssl":true,"syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","0.0.0.0/0"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' TEST_8="$(GTS_ACCOUNT_DOMAIN='peepee' go run ./cmd/gotosocial/... --config-path ./test/test.yaml debug config)" if [ "${TEST_8}" != "${TEST_8_EXPECTED}" ]; then echo "TEST_8 not equal TEST_8_EXPECTED" @@ -85,7 +85,7 @@ else fi echo "TEST_9 Test loading a config file and overriding one of the variables with both an env var and a cli flag. The cli flag should have priority." -TEST_9_EXPECTED='{"account-domain":"","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test.yaml","db-address":"127.0.0.1","db-database":"postgres","db-password":"postgres","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"postgres","email":"","host":"gts.example.org","instance-expose-peers":false,"instance-expose-suspended":false,"letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":10485760,"media-remote-cache-days":30,"media-video-max-size":41943040,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","email","profile","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"someone@example.org","smtp-host":"verycoolemailhost.mail","smtp-password":"smtp-password","smtp-port":8888,"smtp-username":"smtp-username","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","storage-s3-access-key":"","storage-s3-bucket":"","storage-s3-endpoint":"","storage-s3-secret-key":"","storage-s3-use-ssl":true,"syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","0.0.0.0/0"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' +TEST_9_EXPECTED='{"account-domain":"","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test.yaml","db-address":"127.0.0.1","db-database":"postgres","db-password":"postgres","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"postgres","email":"","host":"gts.example.org","instance-expose-peers":false,"instance-expose-suspended":false,"letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-emoji-local-max-size":51200,"media-emoji-remote-max-size":102400,"media-image-max-size":10485760,"media-remote-cache-days":30,"media-video-max-size":41943040,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","email","profile","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"someone@example.org","smtp-host":"verycoolemailhost.mail","smtp-password":"smtp-password","smtp-port":8888,"smtp-username":"smtp-username","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","storage-s3-access-key":"","storage-s3-bucket":"","storage-s3-endpoint":"","storage-s3-secret-key":"","storage-s3-use-ssl":true,"syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","0.0.0.0/0"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' TEST_9="$(GTS_ACCOUNT_DOMAIN='peepee' go run ./cmd/gotosocial/... --config-path ./test/test.yaml --account-domain '' debug config)" if [ "${TEST_9}" != "${TEST_9_EXPECTED}" ]; then echo "TEST_9 not equal TEST_9_EXPECTED" @@ -95,7 +95,7 @@ else fi echo "TEST_10 Test loading a config file from json." -TEST_10_EXPECTED='{"account-domain":"example.org","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test.json","db-address":"127.0.0.1","db-database":"postgres","db-password":"postgres","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"postgres","email":"","host":"gts.example.org","instance-expose-peers":false,"instance-expose-suspended":false,"letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":10485760,"media-remote-cache-days":30,"media-video-max-size":41943040,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","email","profile","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"someone@example.org","smtp-host":"verycoolemailhost.mail","smtp-password":"smtp-password","smtp-port":8888,"smtp-username":"smtp-username","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","storage-s3-access-key":"","storage-s3-bucket":"","storage-s3-endpoint":"","storage-s3-secret-key":"","storage-s3-use-ssl":true,"syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","0.0.0.0/0"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' +TEST_10_EXPECTED='{"account-domain":"example.org","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test.json","db-address":"127.0.0.1","db-database":"postgres","db-password":"postgres","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"postgres","email":"","host":"gts.example.org","instance-expose-peers":false,"instance-expose-suspended":false,"letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":false,"log-level":"info","media-description-max-chars":500,"media-description-min-chars":0,"media-emoji-local-max-size":51200,"media-emoji-remote-max-size":102400,"media-image-max-size":10485760,"media-remote-cache-days":30,"media-video-max-size":41943040,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","email","profile","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"someone@example.org","smtp-host":"verycoolemailhost.mail","smtp-password":"smtp-password","smtp-port":8888,"smtp-username":"smtp-username","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","storage-s3-access-key":"","storage-s3-bucket":"","storage-s3-endpoint":"","storage-s3-secret-key":"","storage-s3-use-ssl":true,"syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","0.0.0.0/0"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' TEST_10="$(go run ./cmd/gotosocial/... --config-path ./test/test.json debug config)" if [ "${TEST_10}" != "${TEST_10_EXPECTED}" ]; then echo "TEST_10 not equal TEST_10_EXPECTED" @@ -105,7 +105,7 @@ else fi echo "TEST_11 Test loading a partial config file. Default values should be used apart from those set in the config file." -TEST_11_EXPECTED='{"account-domain":"peepee.poopoo","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test2.yaml","db-address":"","db-database":"gotosocial","db-password":"","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"","email":"","host":"","instance-expose-peers":false,"instance-expose-suspended":false,"letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":false,"letsencrypt-port":80,"log-db-queries":false,"log-level":"trace","media-description-max-chars":500,"media-description-min-chars":0,"media-image-max-size":10485760,"media-remote-cache-days":30,"media-video-max-size":41943040,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","profile","email","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"GoToSocial","smtp-host":"","smtp-password":"","smtp-port":0,"smtp-username":"","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","storage-s3-access-key":"","storage-s3-bucket":"","storage-s3-endpoint":"","storage-s3-secret-key":"","storage-s3-use-ssl":true,"syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' +TEST_11_EXPECTED='{"account-domain":"peepee.poopoo","accounts-approval-required":true,"accounts-reason-required":true,"accounts-registration-open":true,"advanced-cookies-samesite":"lax","application-name":"gotosocial","bind-address":"0.0.0.0","config-path":"./test/test2.yaml","db-address":"","db-database":"gotosocial","db-password":"","db-port":5432,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"postgres","db-user":"","email":"","host":"","instance-expose-peers":false,"instance-expose-suspended":false,"letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":false,"letsencrypt-port":80,"log-db-queries":false,"log-level":"trace","media-description-max-chars":500,"media-description-min-chars":0,"media-emoji-local-max-size":51200,"media-emoji-remote-max-size":102400,"media-image-max-size":10485760,"media-remote-cache-days":30,"media-video-max-size":41943040,"oidc-client-id":"","oidc-client-secret":"","oidc-enabled":false,"oidc-idp-name":"","oidc-issuer":"","oidc-scopes":["openid","profile","email","groups"],"oidc-skip-verification":false,"password":"","path":"","port":8080,"protocol":"https","smtp-from":"GoToSocial","smtp-host":"","smtp-password":"","smtp-port":0,"smtp-username":"","software-version":"","statuses-cw-max-chars":100,"statuses-max-chars":5000,"statuses-media-max-files":6,"statuses-poll-max-options":6,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/gotosocial/storage","storage-s3-access-key":"","storage-s3-bucket":"","storage-s3-endpoint":"","storage-s3-secret-key":"","storage-s3-use-ssl":true,"syslog-address":"localhost:514","syslog-enabled":false,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32"],"username":"","web-asset-base-dir":"./web/assets/","web-template-base-dir":"./web/template/"}' TEST_11="$(go run ./cmd/gotosocial/... --config-path ./test/test2.yaml debug config)" if [ "${TEST_11}" != "${TEST_11_EXPECTED}" ]; then echo "TEST_11 not equal TEST_11_EXPECTED" diff --git a/test/envparsing.sh b/test/envparsing.sh index 84ff1cca7..539fc1fad 100755 --- a/test/envparsing.sh +++ b/test/envparsing.sh @@ -2,7 +2,7 @@ set -eu -EXPECTED='{"account-domain":"peepee","accounts-approval-required":false,"accounts-reason-required":false,"accounts-registration-open":true,"advanced-cookies-samesite":"strict","application-name":"gts","bind-address":"127.0.0.1","config-path":"./test/test.yaml","db-address":":memory:","db-database":"gotosocial_prod","db-password":"hunter2","db-port":6969,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"sqlite","db-user":"sex-haver","email":"","host":"example.com","instance-expose-peers":true,"instance-expose-suspended":true,"letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":true,"log-level":"info","media-description-max-chars":5000,"media-description-min-chars":69,"media-image-max-size":420,"media-remote-cache-days":30,"media-video-max-size":420,"oidc-client-id":"1234","oidc-client-secret":"shhhh its a secret","oidc-enabled":true,"oidc-idp-name":"sex-haver","oidc-issuer":"whoknows","oidc-scopes":["read","write"],"oidc-skip-verification":true,"password":"","path":"","port":6969,"protocol":"http","smtp-from":"queen@terfisland.org","smtp-host":"example.com","smtp-password":"hunter2","smtp-port":4269,"smtp-username":"sex-haver","software-version":"","statuses-cw-max-chars":420,"statuses-max-chars":69,"statuses-media-max-files":1,"statuses-poll-max-options":1,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/root/store","storage-s3-access-key":"minio","storage-s3-bucket":"gts","storage-s3-endpoint":"localhost:9000","storage-s3-secret-key":"miniostorage","storage-s3-use-ssl":false,"syslog-address":"127.0.0.1:6969","syslog-enabled":true,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","0.0.0.0/0"],"username":"","web-asset-base-dir":"/root","web-template-base-dir":"/root"}' +EXPECTED='{"account-domain":"peepee","accounts-approval-required":false,"accounts-reason-required":false,"accounts-registration-open":true,"advanced-cookies-samesite":"strict","application-name":"gts","bind-address":"127.0.0.1","config-path":"./test/test.yaml","db-address":":memory:","db-database":"gotosocial_prod","db-password":"hunter2","db-port":6969,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"sqlite","db-user":"sex-haver","email":"","host":"example.com","instance-expose-peers":true,"instance-expose-suspended":true,"letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":true,"log-level":"info","media-description-max-chars":5000,"media-description-min-chars":69,"media-emoji-local-max-size":420,"media-emoji-remote-max-size":420,"media-image-max-size":420,"media-remote-cache-days":30,"media-video-max-size":420,"oidc-client-id":"1234","oidc-client-secret":"shhhh its a secret","oidc-enabled":true,"oidc-idp-name":"sex-haver","oidc-issuer":"whoknows","oidc-scopes":["read","write"],"oidc-skip-verification":true,"password":"","path":"","port":6969,"protocol":"http","smtp-from":"queen@terfisland.org","smtp-host":"example.com","smtp-password":"hunter2","smtp-port":4269,"smtp-username":"sex-haver","software-version":"","statuses-cw-max-chars":420,"statuses-max-chars":69,"statuses-media-max-files":1,"statuses-poll-max-options":1,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/root/store","storage-s3-access-key":"minio","storage-s3-bucket":"gts","storage-s3-endpoint":"localhost:9000","storage-s3-secret-key":"miniostorage","storage-s3-use-ssl":false,"syslog-address":"127.0.0.1:6969","syslog-enabled":true,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","0.0.0.0/0"],"username":"","web-asset-base-dir":"/root","web-template-base-dir":"/root"}' # Set all the environment variables to # ensure that these are parsed without panic @@ -35,6 +35,8 @@ GTS_MEDIA_VIDEO_MAX_SIZE=420 \ GTS_MEDIA_DESCRIPTION_MIN_CHARS=69 \ GTS_MEDIA_DESCRIPTION_MAX_CHARS=5000 \ GTS_MEDIA_REMOTE_CACHE_DAYS=30 \ +GTS_MEDIA_EMOJI_LOCAL_MAX_SIZE=420 \ +GTS_MEDIA_EMOJI_REMOTE_MAX_SIZE=420 \ GTS_STORAGE_BACKEND='local' \ GTS_STORAGE_LOCAL_BASE_PATH='/root/store' \ GTS_STORAGE_S3_ACCESS_KEY='minio' \ diff --git a/testrig/config.go b/testrig/config.go index 9de23dfc3..a4df9c004 100644 --- a/testrig/config.go +++ b/testrig/config.go @@ -64,6 +64,8 @@ var testDefaults = config.Configuration{ MediaDescriptionMinChars: 0, MediaDescriptionMaxChars: 500, MediaRemoteCacheDays: 30, + MediaEmojiLocalMaxSize: 51200, // 50kb + MediaEmojiRemoteMaxSize: 102400, // 100kb // the testrig only uses in-memory storage, so we can // safely set this value to 'test' to avoid running storage diff --git a/testrig/media/peglin.gif b/testrig/media/peglin.gif new file mode 100644 index 0000000000000000000000000000000000000000..f14ea3ab50428e07ba59fd340b6062912fb6dbe4 GIT binary patch literal 37796 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

7<}4Ysux)wWN(W6#*sCwy-* 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

L|`z+=0GlH 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 F06RIrJbM5D literal 0 HcmV?d00001 diff --git a/testrig/testmodels.go b/testrig/testmodels.go index 916b38612..b38494642 100644 --- a/testrig/testmodels.go +++ b/testrig/testmodels.go @@ -1954,6 +1954,11 @@ func NewTestFediAttachments(relativePath string) map[string]RemoteAttachmentFile panic(err) } + peglinBytes, err := os.ReadFile(fmt.Sprintf("%s/peglin.gif", relativePath)) + if err != nil { + panic(err) + } + return map[string]RemoteAttachmentFile{ "https://s3-us-west-2.amazonaws.com/plushcity/media_attachments/files/106/867/380/219/163/828/original/88e8758c5f011439.jpg": { Data: beeBytes, @@ -1967,6 +1972,10 @@ func NewTestFediAttachments(relativePath string) map[string]RemoteAttachmentFile Data: massiveFuckingTurnipBytes, ContentType: "image/jpeg", }, + "http://example.org/media/emojis/1781772.gif": { + Data: peglinBytes, + ContentType: "image/gif", + }, } }