diff options
| -rw-r--r-- | internal/api/client/admin/emojicreate_test.go | 40 | ||||
| -rw-r--r-- | internal/media/image.go | 5 | ||||
| -rw-r--r-- | internal/media/manager.go | 6 | ||||
| -rw-r--r-- | internal/media/processingemoji.go | 151 | ||||
| -rw-r--r-- | internal/media/processingmedia.go | 35 | ||||
| -rw-r--r-- | internal/media/types.go | 124 | ||||
| -rw-r--r-- | internal/media/util.go | 123 | ||||
| -rw-r--r-- | internal/processing/admin/emoji.go | 11 | 
8 files changed, 249 insertions, 246 deletions
diff --git a/internal/api/client/admin/emojicreate_test.go b/internal/api/client/admin/emojicreate_test.go index 290b478f7..14b83b534 100644 --- a/internal/api/client/admin/emojicreate_test.go +++ b/internal/api/client/admin/emojicreate_test.go @@ -1,6 +1,8 @@  package admin_test  import ( +	"context" +	"encoding/json"  	"io/ioutil"  	"net/http"  	"net/http/httptest" @@ -8,6 +10,9 @@ import (  	"github.com/stretchr/testify/suite"  	"github.com/superseriousbusiness/gotosocial/internal/api/client/admin" +	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" +	"github.com/superseriousbusiness/gotosocial/internal/db" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/testrig"  ) @@ -43,6 +48,41 @@ func (suite *EmojiCreateTestSuite) TestEmojiCreate() {  	b, err := ioutil.ReadAll(result.Body)  	suite.NoError(err)  	suite.NotEmpty(b) + +	// response should be an api model emoji +	apiEmoji := &apimodel.Emoji{} +	err = json.Unmarshal(b, apiEmoji) +	suite.NoError(err) + +	// appropriate fields should be set +	suite.Equal("rainbow", apiEmoji.Shortcode) +	suite.NotEmpty(apiEmoji.URL) +	suite.NotEmpty(apiEmoji.StaticURL) +	suite.True(apiEmoji.VisibleInPicker) + +	// emoji should be in the db +	dbEmoji := >smodel.Emoji{} +	err = suite.db.GetWhere(context.Background(), []db.Where{{Key: "shortcode", Value: "rainbow"}}, dbEmoji) +	suite.NoError(err) + +	// check fields on the emoji +	suite.NotEmpty(dbEmoji.ID) +	suite.Equal("rainbow", dbEmoji.Shortcode) +	suite.Empty(dbEmoji.Domain) +	suite.Empty(dbEmoji.ImageRemoteURL) +	suite.Empty(dbEmoji.ImageStaticRemoteURL) +	suite.Equal(apiEmoji.URL, dbEmoji.ImageURL) +	suite.Equal(apiEmoji.StaticURL, dbEmoji.ImageURL) +	suite.NotEmpty(dbEmoji.ImagePath) +	suite.NotEmpty(dbEmoji.ImageStaticPath) +	suite.Equal("image/png", dbEmoji.ImageContentType) +	suite.Equal("image/png", dbEmoji.ImageStaticContentType) +	suite.Equal(36702, dbEmoji.ImageFileSize) +	suite.Equal(10413, dbEmoji.ImageStaticFileSize) +	suite.False(dbEmoji.Disabled) +	suite.NotEmpty(dbEmoji.URI) +	suite.True(dbEmoji.VisibleInPicker) +	suite.Empty(dbEmoji.CategoryID)aaaaaaaaa  }  func TestEmojiCreateTestSuite(t *testing.T) { diff --git a/internal/media/image.go b/internal/media/image.go index a5a818206..de4b71210 100644 --- a/internal/media/image.go +++ b/internal/media/image.go @@ -20,21 +20,16 @@ package media  import (  	"bytes" -	"context"  	"errors"  	"fmt"  	"image"  	"image/gif"  	"image/jpeg"  	"image/png" -	"time"  	"github.com/buckket/go-blurhash"  	"github.com/nfnt/resize"  	"github.com/superseriousbusiness/exifremove/pkg/exifremove" -	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -	"github.com/superseriousbusiness/gotosocial/internal/id" -	"github.com/superseriousbusiness/gotosocial/internal/uris"  )  const ( diff --git a/internal/media/manager.go b/internal/media/manager.go index e34471591..7f626271a 100644 --- a/internal/media/manager.go +++ b/internal/media/manager.go @@ -41,7 +41,7 @@ type Manager interface {  	//  	// ai is optional and can be nil. Any additional information about the attachment provided will be put in the database.  	ProcessMedia(ctx context.Context, data DataFunc, accountID string, ai *AdditionalMediaInfo) (*ProcessingMedia, error) -	ProcessEmoji(ctx context.Context, data DataFunc, shortcode string, ai *AdditionalEmojiInfo) (*ProcessingEmoji, error) +	ProcessEmoji(ctx context.Context, data DataFunc, shortcode string, id string, uri string, ai *AdditionalEmojiInfo) (*ProcessingEmoji, error)  	// NumWorkers returns the total number of workers available to this manager.  	NumWorkers() int  	// QueueSize returns the total capacity of the queue. @@ -125,8 +125,8 @@ func (m *manager) ProcessMedia(ctx context.Context, data DataFunc, accountID str  	return processingMedia, nil  } -func (m *manager) ProcessEmoji(ctx context.Context, data DataFunc, shortcode string, ai *AdditionalEmojiInfo) (*ProcessingEmoji, error) { -	processingEmoji, err := m.preProcessEmoji(ctx, data, shortcode, ai) +func (m *manager) ProcessEmoji(ctx context.Context, data DataFunc, shortcode string, id string, uri string, ai *AdditionalEmojiInfo) (*ProcessingEmoji, error) { +	processingEmoji, err := m.preProcessEmoji(ctx, data, shortcode, id, uri, ai)  	if err != nil {  		return nil, err  	} diff --git a/internal/media/processingemoji.go b/internal/media/processingemoji.go index 41754830f..eeccdb281 100644 --- a/internal/media/processingemoji.go +++ b/internal/media/processingemoji.go @@ -28,7 +28,6 @@ import (  	"codeberg.org/gruf/go-store/kv"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -	"github.com/superseriousbusiness/gotosocial/internal/id"  	"github.com/superseriousbusiness/gotosocial/internal/uris"  ) @@ -126,33 +125,28 @@ func (p *ProcessingEmoji) loadStatic(ctx context.Context) (*ImageMeta, error) {  		}  		// set appropriate fields on the emoji based on the static version we derived -		p.attachment.FileMeta.Small = gtsmodel.Small{ -			Width:  static.width, -			Height: static.height, -			Size:   static.size, -			Aspect: static.aspect, -		} -		p.attachment.Thumbnail.FileSize = static.size +		p.emoji.ImageStaticFileSize = len(static.image) -		if err := putOrUpdateAttachment(ctx, p.database, p.attachment); err != nil { +		// update the emoji in the db +		if err := putOrUpdate(ctx, p.database, p.emoji); err != nil {  			p.err = err -			p.thumbstate = errored +			p.staticState = errored  			return nil, err  		} -		// set the thumbnail of this media -		p.thumb = static +		// set the static on the processing emoji +		p.static = static -		// we're done processing the thumbnail! -		p.thumbstate = complete +		// we're done processing the static version of the emoji! +		p.staticState = complete  		fallthrough  	case complete: -		return p.thumb, nil +		return p.static, nil  	case errored:  		return nil, p.err  	} -	return nil, fmt.Errorf("thumbnail processing status %d unknown", p.thumbstate) +	return nil, fmt.Errorf("static processing status %d unknown", p.staticState)  }  func (p *ProcessingEmoji) loadFullSize(ctx context.Context) (*ImageMeta, error) { @@ -161,26 +155,17 @@ func (p *ProcessingEmoji) loadFullSize(ctx context.Context) (*ImageMeta, error)  	switch p.fullSizeState {  	case received: -		var clean []byte  		var err error  		var decoded *ImageMeta -		ct := p.attachment.File.ContentType +		ct := p.emoji.ImageContentType  		switch ct { -		case mimeImageJpeg, mimeImagePng: -			// first 'clean' image by purging exif data from it -			var exifErr error -			if clean, exifErr = purgeExif(p.rawData); exifErr != nil { -				err = exifErr -				break -			} -			decoded, err = decodeImage(clean, ct) +		case mimeImagePng: +			decoded, err = decodeImage(p.rawData, ct)  		case mimeImageGif: -			// gifs are already clean - no exif data to remove -			clean = p.rawData -			decoded, err = decodeGif(clean) +			decoded, err = decodeGif(p.rawData)  		default: -			err = fmt.Errorf("content type %s not a processible image type", ct) +			err = fmt.Errorf("content type %s not a processible emoji type", ct)  		}  		if err != nil { @@ -189,34 +174,17 @@ func (p *ProcessingEmoji) loadFullSize(ctx context.Context) (*ImageMeta, error)  			return nil, err  		} -		// put the full size in storage -		if err := p.storage.Put(p.attachment.File.Path, decoded.image); err != nil { -			p.err = fmt.Errorf("error storing full size image: %s", err) +		// put the full size emoji in storage +		if err := p.storage.Put(p.emoji.ImagePath, decoded.image); err != nil { +			p.err = fmt.Errorf("error storing full size emoji: %s", err)  			p.fullSizeState = errored  			return nil, p.err  		} -		// set appropriate fields on the attachment based on the image we derived -		p.attachment.FileMeta.Original = gtsmodel.Original{ -			Width:  decoded.width, -			Height: decoded.height, -			Size:   decoded.size, -			Aspect: decoded.aspect, -		} -		p.attachment.File.FileSize = decoded.size -		p.attachment.File.UpdatedAt = time.Now() -		p.attachment.Processing = gtsmodel.ProcessingStatusProcessed - -		if err := putOrUpdateAttachment(ctx, p.database, p.attachment); err != nil { -			p.err = err -			p.fullSizeState = errored -			return nil, err -		} -  		// set the fullsize of this media  		p.fullSize = decoded -		// we're done processing the full-size image +		// we're done processing the full-size emoji  		p.fullSizeState = complete  		fallthrough  	case complete: @@ -255,55 +223,24 @@ func (p *ProcessingEmoji) fetchRawData(ctx context.Context) error {  	}  	split := strings.Split(contentType, "/") -	mainType := split[0]  // something like 'image'  	extension := split[1] // something like 'gif'  	// set some additional fields on the emoji now that  	// we know more about what the underlying image actually is -	p.emoji.ImageURL = uris.GenerateURIForAttachment(p.attachment.AccountID, string(TypeAttachment), string(SizeOriginal), p.attachment.ID, extension) -	p.attachment.File.Path = fmt.Sprintf("%s/%s/%s/%s.%s", p.attachment.AccountID, TypeAttachment, SizeOriginal, p.attachment.ID, extension) -	p.attachment.File.ContentType = contentType - -	switch mainType { -	case mimeImage: -		if extension == mimeGif { -			p.attachment.Type = gtsmodel.FileTypeGif -		} else { -			p.attachment.Type = gtsmodel.FileTypeImage -		} -	default: -		return fmt.Errorf("fetchRawData: cannot process mime type %s (yet)", mainType) -	} - -	return nil -} - -// putOrUpdateEmoji is just a convenience function for first trying to PUT the emoji in the database, -// and then if that doesn't work because the emoji already exists, updating it instead. -func putOrUpdateEmoji(ctx context.Context, database db.DB, emoji *gtsmodel.Emoji) error { -	if err := database.Put(ctx, emoji); err != nil { -		if err != db.ErrAlreadyExists { -			return fmt.Errorf("putOrUpdateEmoji: proper error while putting emoji: %s", err) -		} -		if err := database.UpdateByPrimaryKey(ctx, emoji); err != nil { -			return fmt.Errorf("putOrUpdateEmoji: error while updating emoji: %s", err) -		} -	} +	p.emoji.ImageURL = uris.GenerateURIForAttachment(p.instanceAccountID, string(TypeEmoji), string(SizeOriginal), p.emoji.ID, extension) +	p.emoji.ImagePath = fmt.Sprintf("%s/%s/%s/%s.%s", p.instanceAccountID, TypeEmoji, SizeOriginal, p.emoji.ID, extension) +	p.emoji.ImageContentType = contentType +	p.emoji.ImageFileSize = len(p.rawData)  	return nil  } -func (m *manager) preProcessEmoji(ctx context.Context, data DataFunc, shortcode string, ai *AdditionalEmojiInfo) (*ProcessingEmoji, error) { +func (m *manager) preProcessEmoji(ctx context.Context, data DataFunc, shortcode string, id string, uri string, ai *AdditionalEmojiInfo) (*ProcessingEmoji, error) {  	instanceAccount, err := m.db.GetInstanceAccount(ctx, "")  	if err != nil {  		return nil, fmt.Errorf("preProcessEmoji: error fetching this instance account from the db: %s", err)  	} -	id, err := id.NewRandomULID() -	if err != nil { -		return nil, err -	} -  	// populate initial fields on the emoji -- some of these will be overwritten as we proceed  	emoji := >smodel.Emoji{  		ID:                     id, @@ -323,7 +260,7 @@ func (m *manager) preProcessEmoji(ctx context.Context, data DataFunc, shortcode  		ImageStaticFileSize:    0,  		ImageUpdatedAt:         time.Now(),  		Disabled:               false, -		URI:                    "", // we don't know yet +		URI:                    uri,  		VisibleInPicker:        true,  		CategoryID:             "",  	} @@ -332,43 +269,31 @@ func (m *manager) preProcessEmoji(ctx context.Context, data DataFunc, shortcode  	// and overwrite some of the emoji fields if so  	if ai != nil {  		if ai.CreatedAt != nil { -			attachment.CreatedAt = *ai.CreatedAt -		} - -		if ai.StatusID != nil { -			attachment.StatusID = *ai.StatusID -		} - -		if ai.RemoteURL != nil { -			attachment.RemoteURL = *ai.RemoteURL -		} - -		if ai.Description != nil { -			attachment.Description = *ai.Description +			emoji.CreatedAt = *ai.CreatedAt  		} -		if ai.ScheduledStatusID != nil { -			attachment.ScheduledStatusID = *ai.ScheduledStatusID +		if ai.Domain != nil { +			emoji.Domain = *ai.Domain  		} -		if ai.Blurhash != nil { -			attachment.Blurhash = *ai.Blurhash +		if ai.ImageRemoteURL != nil { +			emoji.ImageRemoteURL = *ai.ImageRemoteURL  		} -		if ai.Avatar != nil { -			attachment.Avatar = *ai.Avatar +		if ai.ImageStaticRemoteURL != nil { +			emoji.ImageStaticRemoteURL = *ai.ImageStaticRemoteURL  		} -		if ai.Header != nil { -			attachment.Header = *ai.Header +		if ai.Disabled != nil { +			emoji.Disabled = *ai.Disabled  		} -		if ai.FocusX != nil { -			attachment.FileMeta.Focus.X = *ai.FocusX +		if ai.VisibleInPicker != nil { +			emoji.VisibleInPicker = *ai.VisibleInPicker  		} -		if ai.FocusY != nil { -			attachment.FileMeta.Focus.Y = *ai.FocusY +		if ai.CategoryID != nil { +			emoji.CategoryID = *ai.CategoryID  		}  	} diff --git a/internal/media/processingmedia.go b/internal/media/processingmedia.go index a6e45034f..1bfd7b629 100644 --- a/internal/media/processingmedia.go +++ b/internal/media/processingmedia.go @@ -32,14 +32,6 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/uris"  ) -type processState int - -const ( -	received processState = iota // processing order has been received but not done yet -	complete                     // processing order has been completed successfully -	errored                      // processing order has been completed with an error -) -  // ProcessingMedia represents a piece of media that is currently being processed. It exposes  // various functions for retrieving data from the process.  type ProcessingMedia struct { @@ -103,10 +95,6 @@ func (p *ProcessingMedia) LoadAttachment(ctx context.Context) (*gtsmodel.MediaAt  	return p.attachment, nil  } -func (p *ProcessingMedia) LoadEmoji(ctx context.Context) (*gtsmodel.Emoji, error) { -	return nil, nil -} -  // Finished returns true if processing has finished for both the thumbnail  // and full fized version of this piece of media.  func (p *ProcessingMedia) Finished() bool { @@ -153,9 +141,9 @@ func (p *ProcessingMedia) loadThumb(ctx context.Context) (*ImageMeta, error) {  			Size:   thumb.size,  			Aspect: thumb.aspect,  		} -		p.attachment.Thumbnail.FileSize = thumb.size +		p.attachment.Thumbnail.FileSize = len(thumb.image) -		if err := putOrUpdateAttachment(ctx, p.database, p.attachment); err != nil { +		if err := putOrUpdate(ctx, p.database, p.attachment); err != nil {  			p.err = err  			p.thumbstate = errored  			return nil, err @@ -224,11 +212,11 @@ func (p *ProcessingMedia) loadFullSize(ctx context.Context) (*ImageMeta, error)  			Size:   decoded.size,  			Aspect: decoded.aspect,  		} -		p.attachment.File.FileSize = decoded.size +		p.attachment.File.FileSize = len(decoded.image)  		p.attachment.File.UpdatedAt = time.Now()  		p.attachment.Processing = gtsmodel.ProcessingStatusProcessed -		if err := putOrUpdateAttachment(ctx, p.database, p.attachment); err != nil { +		if err := putOrUpdate(ctx, p.database, p.attachment); err != nil {  			p.err = err  			p.fullSizeState = errored  			return nil, err @@ -299,21 +287,6 @@ func (p *ProcessingMedia) fetchRawData(ctx context.Context) error {  	return nil  } -// putOrUpdateAttachment is just a convenience function for first trying to PUT the attachment in the database, -// and then if that doesn't work because the attachment already exists, updating it instead. -func putOrUpdateAttachment(ctx context.Context, database db.DB, attachment *gtsmodel.MediaAttachment) error { -	if err := database.Put(ctx, attachment); err != nil { -		if err != db.ErrAlreadyExists { -			return fmt.Errorf("putOrUpdateAttachment: proper error while putting attachment: %s", err) -		} -		if err := database.UpdateByPrimaryKey(ctx, attachment); err != nil { -			return fmt.Errorf("putOrUpdateAttachment: error while updating attachment: %s", err) -		} -	} - -	return nil -} -  func (m *manager) preProcessMedia(ctx context.Context, data DataFunc, accountID string, ai *AdditionalMediaInfo) (*ProcessingMedia, error) {  	id, err := id.NewRandomULID()  	if err != nil { diff --git a/internal/media/types.go b/internal/media/types.go index 6426223d1..5b3fe4a41 100644 --- a/internal/media/types.go +++ b/internal/media/types.go @@ -19,15 +19,17 @@  package media  import ( -	"bytes"  	"context" -	"errors" -	"fmt"  	"time" - -	"github.com/h2non/filetype"  ) +// maxFileHeaderBytes represents the maximum amount of bytes we want +// to examine from the beginning of a file to determine its type. +// +// See: https://en.wikipedia.org/wiki/File_format#File_header +// and https://github.com/h2non/filetype +const maxFileHeaderBytes = 262 +  // mime consts  const (  	mimeImage = "image" @@ -42,16 +44,17 @@ const (  	mimeImagePng = mimeImage + "/" + mimePng  ) +type processState int + +const ( +	received processState = iota // processing order has been received but not done yet +	complete                     // processing order has been completed successfully +	errored                      // processing order has been completed with an error +) +  // EmojiMaxBytes is the maximum permitted bytes of an emoji upload (50kb)  // const EmojiMaxBytes = 51200 -// maxFileHeaderBytes represents the maximum amount of bytes we want -// to examine from the beginning of a file to determine its type. -// -// See: https://en.wikipedia.org/wiki/File_format#File_header -// and https://github.com/h2non/filetype -const maxFileHeaderBytes = 262 -  type Size string  const ( @@ -94,89 +97,24 @@ type AdditionalMediaInfo struct {  	FocusY *float32  } +// AdditionalMediaInfo represents additional information +// that should be added to an emoji when processing it.  type AdditionalEmojiInfo struct { -	 +	// Time that this emoji was created; defaults to time.Now(). +	CreatedAt *time.Time +	// Domain the emoji originated from. Blank for this instance's domain. Defaults to "". +	Domain *string +	// URL of this emoji on a remote instance; defaults to "". +	ImageRemoteURL *string +	// URL of the static version of this emoji on a remote instance; defaults to "". +	ImageStaticRemoteURL *string +	// Whether this emoji should be disabled (not shown) on this instance; defaults to false. +	Disabled *bool +	// Whether this emoji should be visible in the instance's emoji picker; defaults to true. +	VisibleInPicker *bool +	// ID of the category this emoji should be placed in; defaults to "". +	CategoryID *string  }  // DataFunc represents a function used to retrieve the raw bytes of a piece of media.  type DataFunc func(ctx context.Context) ([]byte, error) - -// parseContentType parses the MIME content type from a file, returning it as a string in the form (eg., "image/jpeg"). -// Returns an error if the content type is not something we can process. -func parseContentType(content []byte) (string, error) { - -	// read in the first bytes of the file -	fileHeader := make([]byte, maxFileHeaderBytes) -	if _, err := bytes.NewReader(content).Read(fileHeader); err != nil { -		return "", fmt.Errorf("could not read first magic bytes of file: %s", err) -	} - -	kind, err := filetype.Match(fileHeader) -	if err != nil { -		return "", err -	} - -	if kind == filetype.Unknown { -		return "", errors.New("filetype unknown") -	} - -	return kind.MIME.Value, nil -} - -// supportedImage checks mime type of an image against a slice of accepted types, -// and returns True if the mime type is accepted. -func supportedImage(mimeType string) bool { -	acceptedImageTypes := []string{ -		mimeImageJpeg, -		mimeImageGif, -		mimeImagePng, -	} -	for _, accepted := range acceptedImageTypes { -		if mimeType == accepted { -			return true -		} -	} -	return false -} - -// supportedEmoji checks that the content type is image/png -- the only type supported for emoji. -func supportedEmoji(mimeType string) bool { -	acceptedEmojiTypes := []string{ -		mimeImageGif, -		mimeImagePng, -	} -	for _, accepted := range acceptedEmojiTypes { -		if mimeType == accepted { -			return true -		} -	} -	return false -} - -// ParseMediaType converts s to a recognized MediaType, or returns an error if unrecognized -func ParseMediaType(s string) (Type, error) { -	switch s { -	case string(TypeAttachment): -		return TypeAttachment, nil -	case string(TypeHeader): -		return TypeHeader, nil -	case string(TypeAvatar): -		return TypeAvatar, nil -	case string(TypeEmoji): -		return TypeEmoji, nil -	} -	return "", fmt.Errorf("%s not a recognized MediaType", s) -} - -// ParseMediaSize converts s to a recognized MediaSize, or returns an error if unrecognized -func ParseMediaSize(s string) (Size, error) { -	switch s { -	case string(SizeSmall): -		return SizeSmall, nil -	case string(SizeOriginal): -		return SizeOriginal, nil -	case string(SizeStatic): -		return SizeStatic, nil -	} -	return "", fmt.Errorf("%s not a recognized MediaSize", s) -} diff --git a/internal/media/util.go b/internal/media/util.go new file mode 100644 index 000000000..16e874a99 --- /dev/null +++ b/internal/media/util.go @@ -0,0 +1,123 @@ +/* +   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 <http://www.gnu.org/licenses/>. +*/ + +package media + +import ( +	"bytes" +	"context" +	"errors" +	"fmt" + +	"github.com/h2non/filetype" +	"github.com/superseriousbusiness/gotosocial/internal/db" +) + +// parseContentType parses the MIME content type from a file, returning it as a string in the form (eg., "image/jpeg"). +// Returns an error if the content type is not something we can process. +func parseContentType(content []byte) (string, error) { +	// read in the first bytes of the file +	fileHeader := make([]byte, maxFileHeaderBytes) +	if _, err := bytes.NewReader(content).Read(fileHeader); err != nil { +		return "", fmt.Errorf("could not read first magic bytes of file: %s", err) +	} + +	kind, err := filetype.Match(fileHeader) +	if err != nil { +		return "", err +	} + +	if kind == filetype.Unknown { +		return "", errors.New("filetype unknown") +	} + +	return kind.MIME.Value, nil +} + +// supportedImage checks mime type of an image against a slice of accepted types, +// and returns True if the mime type is accepted. +func supportedImage(mimeType string) bool { +	acceptedImageTypes := []string{ +		mimeImageJpeg, +		mimeImageGif, +		mimeImagePng, +	} +	for _, accepted := range acceptedImageTypes { +		if mimeType == accepted { +			return true +		} +	} +	return false +} + +// supportedEmoji checks that the content type is image/png -- the only type supported for emoji. +func supportedEmoji(mimeType string) bool { +	acceptedEmojiTypes := []string{ +		mimeImageGif, +		mimeImagePng, +	} +	for _, accepted := range acceptedEmojiTypes { +		if mimeType == accepted { +			return true +		} +	} +	return false +} + +// ParseMediaType converts s to a recognized MediaType, or returns an error if unrecognized +func ParseMediaType(s string) (Type, error) { +	switch s { +	case string(TypeAttachment): +		return TypeAttachment, nil +	case string(TypeHeader): +		return TypeHeader, nil +	case string(TypeAvatar): +		return TypeAvatar, nil +	case string(TypeEmoji): +		return TypeEmoji, nil +	} +	return "", fmt.Errorf("%s not a recognized MediaType", s) +} + +// ParseMediaSize converts s to a recognized MediaSize, or returns an error if unrecognized +func ParseMediaSize(s string) (Size, error) { +	switch s { +	case string(SizeSmall): +		return SizeSmall, nil +	case string(SizeOriginal): +		return SizeOriginal, nil +	case string(SizeStatic): +		return SizeStatic, nil +	} +	return "", fmt.Errorf("%s not a recognized MediaSize", s) +} + +// putOrUpdate is just a convenience function for first trying to PUT the attachment or emoji in the database, +// and then if that doesn't work because the attachment/emoji already exists, updating it instead. +func putOrUpdate(ctx context.Context, database db.DB, i interface{}) error { +	if err := database.Put(ctx, i); err != nil { +		if err != db.ErrAlreadyExists { +			return fmt.Errorf("putOrUpdate: proper error while putting: %s", err) +		} +		if err := database.UpdateByPrimaryKey(ctx, i); err != nil { +			return fmt.Errorf("putOrUpdate: error while updating: %s", err) +		} +	} + +	return nil +} diff --git a/internal/processing/admin/emoji.go b/internal/processing/admin/emoji.go index 8858dbd02..77fa5102b 100644 --- a/internal/processing/admin/emoji.go +++ b/internal/processing/admin/emoji.go @@ -27,6 +27,8 @@ import (  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/id" +	"github.com/superseriousbusiness/gotosocial/internal/uris"  )  func (p *processor) EmojiCreate(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) { @@ -52,7 +54,14 @@ func (p *processor) EmojiCreate(ctx context.Context, account *gtsmodel.Account,  		return buf.Bytes(), f.Close()  	} -	processingEmoji, err := p.mediaManager.ProcessEmoji(ctx, data, form.Shortcode, nil) +	emojiID, err := id.NewRandomULID() +	if err != nil { +		return nil, fmt.Errorf("error creating id for new emoji: %s", err) +	} + +	emojiURI := uris.GenerateURIForEmoji(emojiID) + +	processingEmoji, err := p.mediaManager.ProcessEmoji(ctx, data, form.Shortcode, emojiID, emojiURI, nil)  	if err != nil {  		return nil, err  	}  | 
