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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%0DZZ;uMocB^N5P5nUc7-Gki-y@wh}% zjMRX{v&72-$*A@KJ%v;W%+kaNfCb&a0RVtFE`+L3DX`X}z`gN46FeL;@BqAnLd!hO zfuh6BJb=!OgwIq%H57q$AkC*b%>UzIFW+dtRY0Wg6Q8Pi&DgBU*}P1w)Ji;45+%^5KfLjp3MBhFet-8IRKit z9ryuI0fkNi)y&cJ#Xca#6F5;6J%KT5lKQzizw!%y3X7lPhBokk8ub7VP{@=>wOMO{ z|1`@F4Gx4Pi)VO-CUXJ_i>2eRrS#grHX;E<5H7P&3lu|?+zC>4xk<7B12X%GD6`Uw zVllCB53+DF$^e5Z%^jG*(*O0$r%KvV*zk!sh%za27xmgHz+ep&uz?RK1D{AzeCj$j zZBxB`k5}W<513QlsGmFiDEu4?MVJ6V-~ep+06@(qZ5q@8l|bVli+sp7X8M3M^|9v4 zt1SB(vY3Dm5Cl2>QbE;Jkf@)M@KhV9)yHyGQe`evt(o_50-xZ3M5WVLh1ChIp9<^M zYzu@y094#r(^3VY*3wfXl~X{yi?1QpeGZ~^+Mo9pBC5BEB8Gq1)b`6du!9Rpe zzbiS)p-kA!QOl9(3S0>MPmvK