emoji code passing muster
This commit is contained in:
parent
c4a533db72
commit
6bf39d0fc1
|
@ -73,6 +73,8 @@ import (
|
||||||
// description: forbidden
|
// description: forbidden
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
|
// '409':
|
||||||
|
// description: conflict -- domain/shortcode combo for emoji already exists
|
||||||
func (m *Module) EmojiCreatePOSTHandler(c *gin.Context) {
|
func (m *Module) EmojiCreatePOSTHandler(c *gin.Context) {
|
||||||
l := logrus.WithFields(logrus.Fields{
|
l := logrus.WithFields(logrus.Fields{
|
||||||
"func": "emojiCreatePOSTHandler",
|
"func": "emojiCreatePOSTHandler",
|
||||||
|
@ -116,10 +118,10 @@ func (m *Module) EmojiCreatePOSTHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
apiEmoji, err := m.processor.AdminEmojiCreate(c.Request.Context(), authed, form)
|
apiEmoji, errWithCode := m.processor.AdminEmojiCreate(c.Request.Context(), authed, form)
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
l.Debugf("error creating emoji: %s", err)
|
l.Debugf("error creating emoji: %s", errWithCode.Error())
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ func (suite *EmojiCreateTestSuite) TestEmojiCreate() {
|
||||||
requestBody, w, err := testrig.CreateMultipartFormData(
|
requestBody, w, err := testrig.CreateMultipartFormData(
|
||||||
"image", "../../../../testrig/media/rainbow-original.png",
|
"image", "../../../../testrig/media/rainbow-original.png",
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"shortcode": "rainbow",
|
"shortcode": "new_emoji",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
@ -55,24 +55,24 @@ func (suite *EmojiCreateTestSuite) TestEmojiCreate() {
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
// appropriate fields should be set
|
// appropriate fields should be set
|
||||||
suite.Equal("rainbow", apiEmoji.Shortcode)
|
suite.Equal("new_emoji", apiEmoji.Shortcode)
|
||||||
suite.NotEmpty(apiEmoji.URL)
|
suite.NotEmpty(apiEmoji.URL)
|
||||||
suite.NotEmpty(apiEmoji.StaticURL)
|
suite.NotEmpty(apiEmoji.StaticURL)
|
||||||
suite.True(apiEmoji.VisibleInPicker)
|
suite.True(apiEmoji.VisibleInPicker)
|
||||||
|
|
||||||
// emoji should be in the db
|
// emoji should be in the db
|
||||||
dbEmoji := >smodel.Emoji{}
|
dbEmoji := >smodel.Emoji{}
|
||||||
err = suite.db.GetWhere(context.Background(), []db.Where{{Key: "shortcode", Value: "rainbow"}}, dbEmoji)
|
err = suite.db.GetWhere(context.Background(), []db.Where{{Key: "shortcode", Value: "new_emoji"}}, dbEmoji)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
// check fields on the emoji
|
// check fields on the emoji
|
||||||
suite.NotEmpty(dbEmoji.ID)
|
suite.NotEmpty(dbEmoji.ID)
|
||||||
suite.Equal("rainbow", dbEmoji.Shortcode)
|
suite.Equal("new_emoji", dbEmoji.Shortcode)
|
||||||
suite.Empty(dbEmoji.Domain)
|
suite.Empty(dbEmoji.Domain)
|
||||||
suite.Empty(dbEmoji.ImageRemoteURL)
|
suite.Empty(dbEmoji.ImageRemoteURL)
|
||||||
suite.Empty(dbEmoji.ImageStaticRemoteURL)
|
suite.Empty(dbEmoji.ImageStaticRemoteURL)
|
||||||
suite.Equal(apiEmoji.URL, dbEmoji.ImageURL)
|
suite.Equal(apiEmoji.URL, dbEmoji.ImageURL)
|
||||||
suite.Equal(apiEmoji.StaticURL, dbEmoji.ImageURL)
|
suite.Equal(apiEmoji.StaticURL, dbEmoji.ImageStaticURL)
|
||||||
suite.NotEmpty(dbEmoji.ImagePath)
|
suite.NotEmpty(dbEmoji.ImagePath)
|
||||||
suite.NotEmpty(dbEmoji.ImageStaticPath)
|
suite.NotEmpty(dbEmoji.ImageStaticPath)
|
||||||
suite.Equal("image/png", dbEmoji.ImageContentType)
|
suite.Equal("image/png", dbEmoji.ImageContentType)
|
||||||
|
@ -82,7 +82,45 @@ func (suite *EmojiCreateTestSuite) TestEmojiCreate() {
|
||||||
suite.False(dbEmoji.Disabled)
|
suite.False(dbEmoji.Disabled)
|
||||||
suite.NotEmpty(dbEmoji.URI)
|
suite.NotEmpty(dbEmoji.URI)
|
||||||
suite.True(dbEmoji.VisibleInPicker)
|
suite.True(dbEmoji.VisibleInPicker)
|
||||||
suite.Empty(dbEmoji.CategoryID)aaaaaaaaa
|
suite.Empty(dbEmoji.CategoryID)
|
||||||
|
|
||||||
|
// emoji should be in storage
|
||||||
|
emojiBytes, err := suite.storage.Get(dbEmoji.ImagePath)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Len(emojiBytes, dbEmoji.ImageFileSize)
|
||||||
|
emojiStaticBytes, err := suite.storage.Get(dbEmoji.ImageStaticPath)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.Len(emojiStaticBytes, dbEmoji.ImageStaticFileSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *EmojiCreateTestSuite) TestEmojiCreateAlreadyExists() {
|
||||||
|
// set up the request -- use a shortcode that already exists for an emoji in the database
|
||||||
|
requestBody, w, err := testrig.CreateMultipartFormData(
|
||||||
|
"image", "../../../../testrig/media/rainbow-original.png",
|
||||||
|
map[string]string{
|
||||||
|
"shortcode": "rainbow",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
bodyBytes := requestBody.Bytes()
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, admin.EmojiPath, w.FormDataContentType())
|
||||||
|
|
||||||
|
// call the handler
|
||||||
|
suite.adminModule.EmojiCreatePOSTHandler(ctx)
|
||||||
|
|
||||||
|
suite.Equal(http.StatusConflict, recorder.Code)
|
||||||
|
|
||||||
|
result := recorder.Result()
|
||||||
|
defer result.Body.Close()
|
||||||
|
|
||||||
|
// check the response
|
||||||
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotEmpty(b)
|
||||||
|
|
||||||
|
suite.Equal(`{"error":"conflict: emoji with shortcode rainbow already exists"}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEmojiCreateTestSuite(t *testing.T) {
|
func TestEmojiCreateTestSuite(t *testing.T) {
|
||||||
|
|
|
@ -122,3 +122,16 @@ func NewErrorInternalError(original error, helpText ...string) WithCode {
|
||||||
code: http.StatusInternalServerError,
|
code: http.StatusInternalServerError,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewErrorConflict returns an ErrorWithCode 409 with the given original error and optional help text.
|
||||||
|
func NewErrorConflict(original error, helpText ...string) WithCode {
|
||||||
|
safe := "conflict"
|
||||||
|
if helpText != nil {
|
||||||
|
safe = safe + ": " + strings.Join(helpText, ": ")
|
||||||
|
}
|
||||||
|
return withCode{
|
||||||
|
original: original,
|
||||||
|
safe: errors.New(safe),
|
||||||
|
code: http.StatusConflict,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -72,6 +72,9 @@ type ProcessingEmoji struct {
|
||||||
storage *kv.KVStore
|
storage *kv.KVStore
|
||||||
|
|
||||||
err error // error created during processing, if any
|
err error // error created during processing, if any
|
||||||
|
|
||||||
|
// track whether this emoji has already been put in the databse
|
||||||
|
insertedInDB bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// EmojiID returns the ID of the underlying emoji without blocking processing.
|
// EmojiID returns the ID of the underlying emoji without blocking processing.
|
||||||
|
@ -94,6 +97,16 @@ func (p *ProcessingEmoji) LoadEmoji(ctx context.Context) (*gtsmodel.Emoji, error
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// store the result in the database before returning it
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
if !p.insertedInDB {
|
||||||
|
if err := p.database.Put(ctx, p.emoji); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p.insertedInDB = true
|
||||||
|
}
|
||||||
|
|
||||||
return p.emoji, nil
|
return p.emoji, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,13 +140,6 @@ 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.emoji.ImageStaticFileSize = len(static.image)
|
p.emoji.ImageStaticFileSize = len(static.image)
|
||||||
|
|
||||||
// update the emoji in the db
|
|
||||||
if err := putOrUpdate(ctx, p.database, p.emoji); err != nil {
|
|
||||||
p.err = err
|
|
||||||
p.staticState = errored
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// set the static on the processing emoji
|
// set the static on the processing emoji
|
||||||
p.static = static
|
p.static = static
|
||||||
|
|
||||||
|
@ -197,7 +203,7 @@ func (p *ProcessingEmoji) loadFullSize(ctx context.Context) (*ImageMeta, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetchRawData calls the data function attached to p if it hasn't been called yet,
|
// fetchRawData calls the data function attached to p if it hasn't been called yet,
|
||||||
// and updates the underlying attachment fields as necessary.
|
// and updates the underlying emoji fields as necessary.
|
||||||
// It should only be called from within a function that already has a lock on p!
|
// It should only be called from within a function that already has a lock on p!
|
||||||
func (p *ProcessingEmoji) fetchRawData(ctx context.Context) error {
|
func (p *ProcessingEmoji) fetchRawData(ctx context.Context) error {
|
||||||
// check if we've already done this and bail early if we have
|
// check if we've already done this and bail early if we have
|
||||||
|
|
|
@ -70,6 +70,9 @@ type ProcessingMedia struct {
|
||||||
storage *kv.KVStore
|
storage *kv.KVStore
|
||||||
|
|
||||||
err error // error created during processing, if any
|
err error // error created during processing, if any
|
||||||
|
|
||||||
|
// track whether this media has already been put in the databse
|
||||||
|
insertedInDB bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// AttachmentID returns the ID of the underlying media attachment without blocking processing.
|
// AttachmentID returns the ID of the underlying media attachment without blocking processing.
|
||||||
|
@ -92,6 +95,16 @@ func (p *ProcessingMedia) LoadAttachment(ctx context.Context) (*gtsmodel.MediaAt
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// store the result in the database before returning it
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
if !p.insertedInDB {
|
||||||
|
if err := p.database.Put(ctx, p.attachment); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p.insertedInDB = true
|
||||||
|
}
|
||||||
|
|
||||||
return p.attachment, nil
|
return p.attachment, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,12 +156,6 @@ func (p *ProcessingMedia) loadThumb(ctx context.Context) (*ImageMeta, error) {
|
||||||
}
|
}
|
||||||
p.attachment.Thumbnail.FileSize = len(thumb.image)
|
p.attachment.Thumbnail.FileSize = len(thumb.image)
|
||||||
|
|
||||||
if err := putOrUpdate(ctx, p.database, p.attachment); err != nil {
|
|
||||||
p.err = err
|
|
||||||
p.thumbstate = errored
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// set the thumbnail of this media
|
// set the thumbnail of this media
|
||||||
p.thumb = thumb
|
p.thumb = thumb
|
||||||
|
|
||||||
|
@ -216,12 +223,6 @@ func (p *ProcessingMedia) loadFullSize(ctx context.Context) (*ImageMeta, error)
|
||||||
p.attachment.File.UpdatedAt = time.Now()
|
p.attachment.File.UpdatedAt = time.Now()
|
||||||
p.attachment.Processing = gtsmodel.ProcessingStatusProcessed
|
p.attachment.Processing = gtsmodel.ProcessingStatusProcessed
|
||||||
|
|
||||||
if err := putOrUpdate(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
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *processor) AdminEmojiCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) {
|
func (p *processor) AdminEmojiCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, gtserror.WithCode) {
|
||||||
return p.adminProcessor.EmojiCreate(ctx, authed.Account, authed.User, form)
|
return p.adminProcessor.EmojiCreate(ctx, authed.Account, authed.User, form)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ type Processor interface {
|
||||||
DomainBlocksGet(ctx context.Context, account *gtsmodel.Account, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode)
|
DomainBlocksGet(ctx context.Context, account *gtsmodel.Account, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode)
|
||||||
DomainBlockGet(ctx context.Context, account *gtsmodel.Account, id string, export bool) (*apimodel.DomainBlock, gtserror.WithCode)
|
DomainBlockGet(ctx context.Context, account *gtsmodel.Account, id string, export bool) (*apimodel.DomainBlock, gtserror.WithCode)
|
||||||
DomainBlockDelete(ctx context.Context, account *gtsmodel.Account, id string) (*apimodel.DomainBlock, gtserror.WithCode)
|
DomainBlockDelete(ctx context.Context, account *gtsmodel.Account, id string) (*apimodel.DomainBlock, gtserror.WithCode)
|
||||||
EmojiCreate(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error)
|
EmojiCreate(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, gtserror.WithCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
type processor struct {
|
type processor struct {
|
||||||
|
|
|
@ -26,14 +26,16 @@ import (
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
"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, gtserror.WithCode) {
|
||||||
if !user.Admin {
|
if !user.Admin {
|
||||||
return nil, fmt.Errorf("user %s not an admin", user.ID)
|
return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("user %s not an admin", user.ID), "user is not an admin")
|
||||||
}
|
}
|
||||||
|
|
||||||
data := func(innerCtx context.Context) ([]byte, error) {
|
data := func(innerCtx context.Context) ([]byte, error) {
|
||||||
|
@ -56,24 +58,27 @@ func (p *processor) EmojiCreate(ctx context.Context, account *gtsmodel.Account,
|
||||||
|
|
||||||
emojiID, err := id.NewRandomULID()
|
emojiID, err := id.NewRandomULID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error creating id for new emoji: %s", err)
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error creating id for new emoji: %s", err), "error creating emoji ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
emojiURI := uris.GenerateURIForEmoji(emojiID)
|
emojiURI := uris.GenerateURIForEmoji(emojiID)
|
||||||
|
|
||||||
processingEmoji, err := p.mediaManager.ProcessEmoji(ctx, data, form.Shortcode, emojiID, emojiURI, nil)
|
processingEmoji, err := p.mediaManager.ProcessEmoji(ctx, data, form.Shortcode, emojiID, emojiURI, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error processing emoji: %s", err), "error processing emoji")
|
||||||
}
|
}
|
||||||
|
|
||||||
emoji, err := processingEmoji.LoadEmoji(ctx)
|
emoji, err := processingEmoji.LoadEmoji(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
if err == db.ErrAlreadyExists {
|
||||||
|
return nil, gtserror.NewErrorConflict(fmt.Errorf("emoji with shortcode %s already exists", form.Shortcode), fmt.Sprintf("emoji with shortcode %s already exists", form.Shortcode))
|
||||||
|
}
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error loading emoji: %s", err), "error loading emoji")
|
||||||
}
|
}
|
||||||
|
|
||||||
apiEmoji, err := p.tc.EmojiToAPIEmoji(ctx, emoji)
|
apiEmoji, err := p.tc.EmojiToAPIEmoji(ctx, emoji)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error converting emoji to apitype: %s", err)
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting emoji: %s", err), "error converting emoji to api representation")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &apiEmoji, nil
|
return &apiEmoji, nil
|
||||||
|
|
|
@ -96,7 +96,7 @@ type Processor interface {
|
||||||
AccountBlockRemove(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
|
AccountBlockRemove(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
|
||||||
|
|
||||||
// AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form.
|
// AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form.
|
||||||
AdminEmojiCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error)
|
AdminEmojiCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, gtserror.WithCode)
|
||||||
// AdminDomainBlockCreate handles the creation of a new domain block by an admin, using the given form.
|
// AdminDomainBlockCreate handles the creation of a new domain block by an admin, using the given form.
|
||||||
AdminDomainBlockCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.DomainBlockCreateRequest) (*apimodel.DomainBlock, gtserror.WithCode)
|
AdminDomainBlockCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.DomainBlockCreateRequest) (*apimodel.DomainBlock, gtserror.WithCode)
|
||||||
// AdminDomainBlocksImport handles the import of multiple domain blocks by an admin, using the given form.
|
// AdminDomainBlocksImport handles the import of multiple domain blocks by an admin, using the given form.
|
||||||
|
|
Loading…
Reference in New Issue