mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 05:52:25 -05:00 
			
		
		
		
	start fixing up emoji processing code
This commit is contained in:
		
					parent
					
						
							
								33ca5513ad
							
						
					
				
			
			
				commit
				
					
						c4a533db72
					
				
			
		
					 8 changed files with 249 additions and 246 deletions
				
			
		|  | @ -1,6 +1,8 @@ | ||||||
| package admin_test | package admin_test | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"encoding/json" | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/http/httptest" | 	"net/http/httptest" | ||||||
|  | @ -8,6 +10,9 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"github.com/stretchr/testify/suite" | 	"github.com/stretchr/testify/suite" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/client/admin" | 	"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" | 	"github.com/superseriousbusiness/gotosocial/testrig" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -43,6 +48,41 @@ func (suite *EmojiCreateTestSuite) TestEmojiCreate() { | ||||||
| 	b, err := ioutil.ReadAll(result.Body) | 	b, err := ioutil.ReadAll(result.Body) | ||||||
| 	suite.NoError(err) | 	suite.NoError(err) | ||||||
| 	suite.NotEmpty(b) | 	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) { | func TestEmojiCreateTestSuite(t *testing.T) { | ||||||
|  |  | ||||||
|  | @ -20,21 +20,16 @@ package media | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"context" |  | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"image" | 	"image" | ||||||
| 	"image/gif" | 	"image/gif" | ||||||
| 	"image/jpeg" | 	"image/jpeg" | ||||||
| 	"image/png" | 	"image/png" | ||||||
| 	"time" |  | ||||||
| 
 | 
 | ||||||
| 	"github.com/buckket/go-blurhash" | 	"github.com/buckket/go-blurhash" | ||||||
| 	"github.com/nfnt/resize" | 	"github.com/nfnt/resize" | ||||||
| 	"github.com/superseriousbusiness/exifremove/pkg/exifremove" | 	"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 ( | const ( | ||||||
|  |  | ||||||
|  | @ -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. | 	// 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) | 	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 returns the total number of workers available to this manager. | ||||||
| 	NumWorkers() int | 	NumWorkers() int | ||||||
| 	// QueueSize returns the total capacity of the queue. | 	// 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 | 	return processingMedia, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (m *manager) ProcessEmoji(ctx context.Context, data DataFunc, shortcode string, ai *AdditionalEmojiInfo) (*ProcessingEmoji, error) { | 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, ai) | 	processingEmoji, err := m.preProcessEmoji(ctx, data, shortcode, id, uri, ai) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -28,7 +28,6 @@ import ( | ||||||
| 	"codeberg.org/gruf/go-store/kv" | 	"codeberg.org/gruf/go-store/kv" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/id" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/uris" | 	"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 | 		// set appropriate fields on the emoji based on the static version we derived | ||||||
| 		p.attachment.FileMeta.Small = gtsmodel.Small{ | 		p.emoji.ImageStaticFileSize = len(static.image) | ||||||
| 			Width:  static.width, |  | ||||||
| 			Height: static.height, |  | ||||||
| 			Size:   static.size, |  | ||||||
| 			Aspect: static.aspect, |  | ||||||
| 		} |  | ||||||
| 		p.attachment.Thumbnail.FileSize = static.size |  | ||||||
| 
 | 
 | ||||||
| 		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.err = err | ||||||
| 			p.thumbstate = errored | 			p.staticState = errored | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		// set the thumbnail of this media | 		// set the static on the processing emoji | ||||||
| 		p.thumb = static | 		p.static = static | ||||||
| 
 | 
 | ||||||
| 		// we're done processing the thumbnail! | 		// we're done processing the static version of the emoji! | ||||||
| 		p.thumbstate = complete | 		p.staticState = complete | ||||||
| 		fallthrough | 		fallthrough | ||||||
| 	case complete: | 	case complete: | ||||||
| 		return p.thumb, nil | 		return p.static, nil | ||||||
| 	case errored: | 	case errored: | ||||||
| 		return nil, p.err | 		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) { | 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 { | 	switch p.fullSizeState { | ||||||
| 	case received: | 	case received: | ||||||
| 		var clean []byte |  | ||||||
| 		var err error | 		var err error | ||||||
| 		var decoded *ImageMeta | 		var decoded *ImageMeta | ||||||
| 
 | 
 | ||||||
| 		ct := p.attachment.File.ContentType | 		ct := p.emoji.ImageContentType | ||||||
| 		switch ct { | 		switch ct { | ||||||
| 		case mimeImageJpeg, mimeImagePng: | 		case mimeImagePng: | ||||||
| 			// first 'clean' image by purging exif data from it | 			decoded, err = decodeImage(p.rawData, ct) | ||||||
| 			var exifErr error |  | ||||||
| 			if clean, exifErr = purgeExif(p.rawData); exifErr != nil { |  | ||||||
| 				err = exifErr |  | ||||||
| 				break |  | ||||||
| 			} |  | ||||||
| 			decoded, err = decodeImage(clean, ct) |  | ||||||
| 		case mimeImageGif: | 		case mimeImageGif: | ||||||
| 			// gifs are already clean - no exif data to remove | 			decoded, err = decodeGif(p.rawData) | ||||||
| 			clean = p.rawData |  | ||||||
| 			decoded, err = decodeGif(clean) |  | ||||||
| 		default: | 		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 { | 		if err != nil { | ||||||
|  | @ -189,34 +174,17 @@ func (p *ProcessingEmoji) loadFullSize(ctx context.Context) (*ImageMeta, error) | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		// put the full size in storage | 		// put the full size emoji in storage | ||||||
| 		if err := p.storage.Put(p.attachment.File.Path, decoded.image); err != nil { | 		if err := p.storage.Put(p.emoji.ImagePath, decoded.image); err != nil { | ||||||
| 			p.err = fmt.Errorf("error storing full size image: %s", err) | 			p.err = fmt.Errorf("error storing full size emoji: %s", err) | ||||||
| 			p.fullSizeState = errored | 			p.fullSizeState = errored | ||||||
| 			return nil, p.err | 			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 | 		// set the fullsize of this media | ||||||
| 		p.fullSize = decoded | 		p.fullSize = decoded | ||||||
| 
 | 
 | ||||||
| 		// we're done processing the full-size image | 		// we're done processing the full-size emoji | ||||||
| 		p.fullSizeState = complete | 		p.fullSizeState = complete | ||||||
| 		fallthrough | 		fallthrough | ||||||
| 	case complete: | 	case complete: | ||||||
|  | @ -255,55 +223,24 @@ func (p *ProcessingEmoji) fetchRawData(ctx context.Context) error { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	split := strings.Split(contentType, "/") | 	split := strings.Split(contentType, "/") | ||||||
| 	mainType := split[0]  // something like 'image' |  | ||||||
| 	extension := split[1] // something like 'gif' | 	extension := split[1] // something like 'gif' | ||||||
| 
 | 
 | ||||||
| 	// set some additional fields on the emoji now that | 	// set some additional fields on the emoji now that | ||||||
| 	// we know more about what the underlying image actually is | 	// 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.emoji.ImageURL = uris.GenerateURIForAttachment(p.instanceAccountID, string(TypeEmoji), string(SizeOriginal), p.emoji.ID, extension) | ||||||
| 	p.attachment.File.Path = fmt.Sprintf("%s/%s/%s/%s.%s", p.attachment.AccountID, TypeAttachment, SizeOriginal, p.attachment.ID, extension) | 	p.emoji.ImagePath = fmt.Sprintf("%s/%s/%s/%s.%s", p.instanceAccountID, TypeEmoji, SizeOriginal, p.emoji.ID, extension) | ||||||
| 	p.attachment.File.ContentType = contentType | 	p.emoji.ImageContentType = contentType | ||||||
| 
 | 	p.emoji.ImageFileSize = len(p.rawData) | ||||||
| 	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 | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // putOrUpdateEmoji is just a convenience function for first trying to PUT the emoji in the database, | func (m *manager) preProcessEmoji(ctx context.Context, data DataFunc, shortcode string, id string, uri string, ai *AdditionalEmojiInfo) (*ProcessingEmoji, error) { | ||||||
| // 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) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (m *manager) preProcessEmoji(ctx context.Context, data DataFunc, shortcode string, ai *AdditionalEmojiInfo) (*ProcessingEmoji, error) { |  | ||||||
| 	instanceAccount, err := m.db.GetInstanceAccount(ctx, "") | 	instanceAccount, err := m.db.GetInstanceAccount(ctx, "") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("preProcessEmoji: error fetching this instance account from the db: %s", err) | 		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 | 	// populate initial fields on the emoji -- some of these will be overwritten as we proceed | ||||||
| 	emoji := >smodel.Emoji{ | 	emoji := >smodel.Emoji{ | ||||||
| 		ID:                     id, | 		ID:                     id, | ||||||
|  | @ -323,7 +260,7 @@ func (m *manager) preProcessEmoji(ctx context.Context, data DataFunc, shortcode | ||||||
| 		ImageStaticFileSize:    0, | 		ImageStaticFileSize:    0, | ||||||
| 		ImageUpdatedAt:         time.Now(), | 		ImageUpdatedAt:         time.Now(), | ||||||
| 		Disabled:               false, | 		Disabled:               false, | ||||||
| 		URI:                    "", // we don't know yet | 		URI:                    uri, | ||||||
| 		VisibleInPicker:        true, | 		VisibleInPicker:        true, | ||||||
| 		CategoryID:             "", | 		CategoryID:             "", | ||||||
| 	} | 	} | ||||||
|  | @ -332,43 +269,31 @@ func (m *manager) preProcessEmoji(ctx context.Context, data DataFunc, shortcode | ||||||
| 	// and overwrite some of the emoji fields if so | 	// and overwrite some of the emoji fields if so | ||||||
| 	if ai != nil { | 	if ai != nil { | ||||||
| 		if ai.CreatedAt != nil { | 		if ai.CreatedAt != nil { | ||||||
| 			attachment.CreatedAt = *ai.CreatedAt | 			emoji.CreatedAt = *ai.CreatedAt | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if ai.StatusID != nil { | 		if ai.Domain != nil { | ||||||
| 			attachment.StatusID = *ai.StatusID | 			emoji.Domain = *ai.Domain | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if ai.RemoteURL != nil { | 		if ai.ImageRemoteURL != nil { | ||||||
| 			attachment.RemoteURL = *ai.RemoteURL | 			emoji.ImageRemoteURL = *ai.ImageRemoteURL | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if ai.Description != nil { | 		if ai.ImageStaticRemoteURL != nil { | ||||||
| 			attachment.Description = *ai.Description | 			emoji.ImageStaticRemoteURL = *ai.ImageStaticRemoteURL | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if ai.ScheduledStatusID != nil { | 		if ai.Disabled != nil { | ||||||
| 			attachment.ScheduledStatusID = *ai.ScheduledStatusID | 			emoji.Disabled = *ai.Disabled | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if ai.Blurhash != nil { | 		if ai.VisibleInPicker != nil { | ||||||
| 			attachment.Blurhash = *ai.Blurhash | 			emoji.VisibleInPicker = *ai.VisibleInPicker | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if ai.Avatar != nil { | 		if ai.CategoryID != nil { | ||||||
| 			attachment.Avatar = *ai.Avatar | 			emoji.CategoryID = *ai.CategoryID | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		if ai.Header != nil { |  | ||||||
| 			attachment.Header = *ai.Header |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		if ai.FocusX != nil { |  | ||||||
| 			attachment.FileMeta.Focus.X = *ai.FocusX |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		if ai.FocusY != nil { |  | ||||||
| 			attachment.FileMeta.Focus.Y = *ai.FocusY |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -32,14 +32,6 @@ import ( | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/uris" | 	"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 | // ProcessingMedia represents a piece of media that is currently being processed. It exposes | ||||||
| // various functions for retrieving data from the process. | // various functions for retrieving data from the process. | ||||||
| type ProcessingMedia struct { | type ProcessingMedia struct { | ||||||
|  | @ -103,10 +95,6 @@ func (p *ProcessingMedia) LoadAttachment(ctx context.Context) (*gtsmodel.MediaAt | ||||||
| 	return p.attachment, nil | 	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 | // Finished returns true if processing has finished for both the thumbnail | ||||||
| // and full fized version of this piece of media. | // and full fized version of this piece of media. | ||||||
| func (p *ProcessingMedia) Finished() bool { | func (p *ProcessingMedia) Finished() bool { | ||||||
|  | @ -153,9 +141,9 @@ func (p *ProcessingMedia) loadThumb(ctx context.Context) (*ImageMeta, error) { | ||||||
| 			Size:   thumb.size, | 			Size:   thumb.size, | ||||||
| 			Aspect: thumb.aspect, | 			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.err = err | ||||||
| 			p.thumbstate = errored | 			p.thumbstate = errored | ||||||
| 			return nil, err | 			return nil, err | ||||||
|  | @ -224,11 +212,11 @@ func (p *ProcessingMedia) loadFullSize(ctx context.Context) (*ImageMeta, error) | ||||||
| 			Size:   decoded.size, | 			Size:   decoded.size, | ||||||
| 			Aspect: decoded.aspect, | 			Aspect: decoded.aspect, | ||||||
| 		} | 		} | ||||||
| 		p.attachment.File.FileSize = decoded.size | 		p.attachment.File.FileSize = len(decoded.image) | ||||||
| 		p.attachment.File.UpdatedAt = time.Now() | 		p.attachment.File.UpdatedAt = time.Now() | ||||||
| 		p.attachment.Processing = gtsmodel.ProcessingStatusProcessed | 		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.err = err | ||||||
| 			p.fullSizeState = errored | 			p.fullSizeState = errored | ||||||
| 			return nil, err | 			return nil, err | ||||||
|  | @ -299,21 +287,6 @@ func (p *ProcessingMedia) fetchRawData(ctx context.Context) error { | ||||||
| 	return nil | 	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) { | func (m *manager) preProcessMedia(ctx context.Context, data DataFunc, accountID string, ai *AdditionalMediaInfo) (*ProcessingMedia, error) { | ||||||
| 	id, err := id.NewRandomULID() | 	id, err := id.NewRandomULID() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  |  | ||||||
|  | @ -19,15 +19,17 @@ | ||||||
| package media | package media | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bytes" |  | ||||||
| 	"context" | 	"context" | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 	"time" | 	"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 | // mime consts | ||||||
| const ( | const ( | ||||||
| 	mimeImage = "image" | 	mimeImage = "image" | ||||||
|  | @ -42,16 +44,17 @@ const ( | ||||||
| 	mimeImagePng = mimeImage + "/" + mimePng | 	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) | // EmojiMaxBytes is the maximum permitted bytes of an emoji upload (50kb) | ||||||
| // const EmojiMaxBytes = 51200 | // 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 | type Size string | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
|  | @ -94,89 +97,24 @@ type AdditionalMediaInfo struct { | ||||||
| 	FocusY *float32 | 	FocusY *float32 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // AdditionalMediaInfo represents additional information | ||||||
|  | // that should be added to an emoji when processing it. | ||||||
| type AdditionalEmojiInfo struct { | 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. | // DataFunc represents a function used to retrieve the raw bytes of a piece of media. | ||||||
| type DataFunc func(ctx context.Context) ([]byte, error) | 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) |  | ||||||
| } |  | ||||||
|  |  | ||||||
							
								
								
									
										123
									
								
								internal/media/util.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								internal/media/util.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||||
|  | } | ||||||
|  | @ -27,6 +27,8 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"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) { | 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() | 		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 { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue