mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-11-04 16:02:25 -06:00
encode gifs properly
This commit is contained in:
parent
2fa5519d55
commit
2e7ac10d00
15 changed files with 304 additions and 66 deletions
|
|
@ -149,7 +149,7 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newStatus.Mentions = menchies
|
newStatus.GTSMentions = menchies
|
||||||
|
|
||||||
// convert tags to *gtsmodel.Tag
|
// convert tags to *gtsmodel.Tag
|
||||||
tags, err := m.db.TagStringsToTags(util.DeriveHashtags(form.Status), authed.Account.ID, thisStatusID)
|
tags, err := m.db.TagStringsToTags(util.DeriveHashtags(form.Status), authed.Account.ID, thisStatusID)
|
||||||
|
|
@ -158,7 +158,7 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "error generating hashtags from status"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "error generating hashtags from status"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
newStatus.Tags = tags
|
newStatus.GTSTags = tags
|
||||||
|
|
||||||
// convert emojis to *gtsmodel.Emoji
|
// convert emojis to *gtsmodel.Emoji
|
||||||
emojis, err := m.db.EmojiStringsToEmojis(util.DeriveEmojis(form.Status), authed.Account.ID, thisStatusID)
|
emojis, err := m.db.EmojiStringsToEmojis(util.DeriveEmojis(form.Status), authed.Account.ID, thisStatusID)
|
||||||
|
|
@ -167,7 +167,7 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "error generating emojis from status"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "error generating emojis from status"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
newStatus.Emojis = emojis
|
newStatus.GTSEmojis = emojis
|
||||||
|
|
||||||
/*
|
/*
|
||||||
FROM THIS POINT ONWARDS WE ARE HAPPY WITH THE STATUS -- it is valid and we will try to create it
|
FROM THIS POINT ONWARDS WE ARE HAPPY WITH THE STATUS -- it is valid and we will try to create it
|
||||||
|
|
@ -180,7 +180,7 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// change the status ID of the media attachments to the new status
|
// change the status ID of the media attachments to the new status
|
||||||
for _, a := range newStatus.Attachments {
|
for _, a := range newStatus.GTSMediaAttachments {
|
||||||
a.StatusID = newStatus.ID
|
a.StatusID = newStatus.ID
|
||||||
a.UpdatedAt = time.Now()
|
a.UpdatedAt = time.Now()
|
||||||
if err := m.db.UpdateByID(a.ID, a); err != nil {
|
if err := m.db.UpdateByID(a.ID, a); err != nil {
|
||||||
|
|
@ -207,7 +207,7 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
mastoAttachments := []mastotypes.Attachment{}
|
mastoAttachments := []mastotypes.Attachment{}
|
||||||
for _, a := range newStatus.Attachments {
|
for _, a := range newStatus.GTSMediaAttachments {
|
||||||
ma, err := m.mastoConverter.AttachmentToMasto(a)
|
ma, err := m.mastoConverter.AttachmentToMasto(a)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
|
@ -217,7 +217,7 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
mastoMentions := []mastotypes.Mention{}
|
mastoMentions := []mastotypes.Mention{}
|
||||||
for _, gtsm := range newStatus.Mentions {
|
for _, gtsm := range newStatus.GTSMentions {
|
||||||
mm, err := m.mastoConverter.MentionToMasto(gtsm)
|
mm, err := m.mastoConverter.MentionToMasto(gtsm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
|
@ -433,7 +433,8 @@ func (m *statusModule) parseMediaIDs(form *advancedStatusCreateForm, thisAccount
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
attachments := []*gtsmodel.MediaAttachment{}
|
GTSMediaAttachments := []*gtsmodel.MediaAttachment{}
|
||||||
|
Attachments := []string{}
|
||||||
for _, mediaID := range form.MediaIDs {
|
for _, mediaID := range form.MediaIDs {
|
||||||
// check these attachments exist
|
// check these attachments exist
|
||||||
a := >smodel.MediaAttachment{}
|
a := >smodel.MediaAttachment{}
|
||||||
|
|
@ -448,9 +449,11 @@ func (m *statusModule) parseMediaIDs(form *advancedStatusCreateForm, thisAccount
|
||||||
if a.StatusID != "" || a.ScheduledStatusID != "" {
|
if a.StatusID != "" || a.ScheduledStatusID != "" {
|
||||||
return fmt.Errorf("media with id %s is already attached to a status", mediaID)
|
return fmt.Errorf("media with id %s is already attached to a status", mediaID)
|
||||||
}
|
}
|
||||||
attachments = append(attachments, a)
|
GTSMediaAttachments = append(GTSMediaAttachments, a)
|
||||||
|
Attachments = append(Attachments, a.ID)
|
||||||
}
|
}
|
||||||
status.Attachments = attachments
|
status.GTSMediaAttachments = GTSMediaAttachments
|
||||||
|
status.Attachments = Attachments
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,8 @@ func (suite *StatusCreateTestSuite) TearDownTest() {
|
||||||
TESTING: StatusCreatePOSTHandler
|
TESTING: StatusCreatePOSTHandler
|
||||||
*/
|
*/
|
||||||
|
|
||||||
func (suite *StatusCreateTestSuite) TestStatusCreatePOSTHandlerSuccessful() {
|
// Post a new status with some custom visibility settings
|
||||||
|
func (suite *StatusCreateTestSuite) TestPostNewStatus() {
|
||||||
|
|
||||||
t := suite.testTokens["local_account_1"]
|
t := suite.testTokens["local_account_1"]
|
||||||
oauthToken := oauth.PGTokenToOauthToken(t)
|
oauthToken := oauth.PGTokenToOauthToken(t)
|
||||||
|
|
@ -160,7 +161,8 @@ func (suite *StatusCreateTestSuite) TestStatusCreatePOSTHandlerSuccessful() {
|
||||||
assert.Equal(suite.T(), mastomodel.VisibilityPrivate, statusReply.Visibility)
|
assert.Equal(suite.T(), mastomodel.VisibilityPrivate, statusReply.Visibility)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *StatusCreateTestSuite) TestStatusCreatePOSTHandlerReplyToFail() {
|
// Try to reply to a status that doesn't exist
|
||||||
|
func (suite *StatusCreateTestSuite) TestReplyToNonexistentStatus() {
|
||||||
t := suite.testTokens["local_account_1"]
|
t := suite.testTokens["local_account_1"]
|
||||||
oauthToken := oauth.PGTokenToOauthToken(t)
|
oauthToken := oauth.PGTokenToOauthToken(t)
|
||||||
|
|
||||||
|
|
@ -190,7 +192,8 @@ func (suite *StatusCreateTestSuite) TestStatusCreatePOSTHandlerReplyToFail() {
|
||||||
assert.Equal(suite.T(), `{"error":"status with id 3759e7ef-8ee1-4c0c-86f6-8b70b9ad3d50 not replyable because it doesn't exist"}`, string(b))
|
assert.Equal(suite.T(), `{"error":"status with id 3759e7ef-8ee1-4c0c-86f6-8b70b9ad3d50 not replyable because it doesn't exist"}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *StatusCreateTestSuite) TestStatusCreatePOSTHandlerReplyToLocalSuccess() {
|
// Post a reply to the status of a local user that allows replies.
|
||||||
|
func (suite *StatusCreateTestSuite) TestReplyToLocalStatus() {
|
||||||
t := suite.testTokens["local_account_1"]
|
t := suite.testTokens["local_account_1"]
|
||||||
oauthToken := oauth.PGTokenToOauthToken(t)
|
oauthToken := oauth.PGTokenToOauthToken(t)
|
||||||
|
|
||||||
|
|
@ -229,6 +232,63 @@ func (suite *StatusCreateTestSuite) TestStatusCreatePOSTHandlerReplyToLocalSucce
|
||||||
assert.Len(suite.T(), statusReply.Mentions, 1)
|
assert.Len(suite.T(), statusReply.Mentions, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Take a media file which is currently not associated with a status, and attach it to a new status.
|
||||||
|
func (suite *StatusCreateTestSuite) TestAttachNewMediaSuccess() {
|
||||||
|
t := suite.testTokens["local_account_1"]
|
||||||
|
oauthToken := oauth.PGTokenToOauthToken(t)
|
||||||
|
|
||||||
|
// setup
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx, _ := gin.CreateTestContext(recorder)
|
||||||
|
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||||
|
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
||||||
|
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||||
|
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||||
|
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", basePath), nil) // the endpoint we're hitting
|
||||||
|
ctx.Request.Form = url.Values{
|
||||||
|
"status": {"here's an image attachment"},
|
||||||
|
"media_ids": {"7a3b9f77-ab30-461e-bdd8-e64bd1db3008"},
|
||||||
|
}
|
||||||
|
suite.statusModule.statusCreatePOSTHandler(ctx)
|
||||||
|
|
||||||
|
// check response
|
||||||
|
suite.EqualValues(http.StatusOK, recorder.Code)
|
||||||
|
|
||||||
|
result := recorder.Result()
|
||||||
|
defer result.Body.Close()
|
||||||
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
|
assert.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
fmt.Println(string(b))
|
||||||
|
|
||||||
|
statusReply := &mastomodel.Status{}
|
||||||
|
err = json.Unmarshal(b, statusReply)
|
||||||
|
assert.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
assert.Equal(suite.T(), "", statusReply.SpoilerText)
|
||||||
|
assert.Equal(suite.T(), "here's an image attachment", statusReply.Content)
|
||||||
|
assert.False(suite.T(), statusReply.Sensitive)
|
||||||
|
assert.Equal(suite.T(), mastomodel.VisibilityPublic, statusReply.Visibility)
|
||||||
|
|
||||||
|
// there should be one media attachment
|
||||||
|
assert.Len(suite.T(), statusReply.MediaAttachments, 1)
|
||||||
|
|
||||||
|
// get the updated media attachment from the database
|
||||||
|
gtsAttachment := >smodel.MediaAttachment{}
|
||||||
|
err = suite.db.GetByID(statusReply.MediaAttachments[0].ID, gtsAttachment)
|
||||||
|
assert.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
// convert it to a masto attachment
|
||||||
|
gtsAttachmentAsMasto, err := suite.mastoConverter.AttachmentToMasto(gtsAttachment)
|
||||||
|
assert.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
// compare it with what we have now
|
||||||
|
assert.EqualValues(suite.T(), statusReply.MediaAttachments[0], gtsAttachmentAsMasto)
|
||||||
|
|
||||||
|
// the status id of the attachment should now be set to the id of the status we just created
|
||||||
|
assert.Equal(suite.T(), statusReply.ID, gtsAttachment.StatusID)
|
||||||
|
}
|
||||||
|
|
||||||
func TestStatusCreateTestSuite(t *testing.T) {
|
func TestStatusCreateTestSuite(t *testing.T) {
|
||||||
suite.Run(t, new(StatusCreateTestSuite))
|
suite.Run(t, new(StatusCreateTestSuite))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -117,8 +117,8 @@ func GetDefaults() Defaults {
|
||||||
AccountsRequireApproval: true,
|
AccountsRequireApproval: true,
|
||||||
AccountsReasonRequired: true,
|
AccountsReasonRequired: true,
|
||||||
|
|
||||||
MediaMaxImageSize: 1048576, //1mb
|
MediaMaxImageSize: 2097152, //2mb
|
||||||
MediaMaxVideoSize: 5242880, //5mb
|
MediaMaxVideoSize: 10485760, //10mb
|
||||||
MediaMinDescriptionChars: 0,
|
MediaMinDescriptionChars: 0,
|
||||||
MediaMaxDescriptionChars: 500,
|
MediaMaxDescriptionChars: 500,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ const (
|
||||||
// FileTypeImage is for jpegs and pngs
|
// FileTypeImage is for jpegs and pngs
|
||||||
FileTypeImage FileType = "image"
|
FileTypeImage FileType = "image"
|
||||||
// FileTypeGif is for native gifs and soundless videos that have been converted to gifs
|
// FileTypeGif is for native gifs and soundless videos that have been converted to gifs
|
||||||
FileTypeGif FileType = "gifv"
|
FileTypeGif FileType = "gif"
|
||||||
// FileTypeAudio is for audio-only files (no video)
|
// FileTypeAudio is for audio-only files (no video)
|
||||||
FileTypeAudio FileType = "audio"
|
FileTypeAudio FileType = "audio"
|
||||||
// FileTypeVideo is for files with audio + visual
|
// FileTypeVideo is for files with audio + visual
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ type Status struct {
|
||||||
URL string `pg:",unique"`
|
URL string `pg:",unique"`
|
||||||
// the html-formatted content of this status
|
// the html-formatted content of this status
|
||||||
Content string
|
Content string
|
||||||
|
// Database IDs of any media attachments associated with this status
|
||||||
|
Attachments []string
|
||||||
// when was this status created?
|
// when was this status created?
|
||||||
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
||||||
// when was this status updated?
|
// when was this status updated?
|
||||||
|
|
@ -62,21 +64,21 @@ type Status struct {
|
||||||
NON-DATABASE FIELDS
|
NON-DATABASE FIELDS
|
||||||
|
|
||||||
These are for convenience while passing the status around internally,
|
These are for convenience while passing the status around internally,
|
||||||
but these fields should never be put in the db.
|
but these fields should *never* be put in the db.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Mentions created in this status
|
// Mentions created in this status
|
||||||
Mentions []*Mention `pg:"-"`
|
GTSMentions []*Mention `pg:"-"`
|
||||||
// Hashtags used in this status
|
// Hashtags used in this status
|
||||||
Tags []*Tag `pg:"-"`
|
GTSTags []*Tag `pg:"-"`
|
||||||
// Emojis used in this status
|
// Emojis used in this status
|
||||||
Emojis []*Emoji `pg:"-"`
|
GTSEmojis []*Emoji `pg:"-"`
|
||||||
// Attachments used in this status
|
// MediaAttachments used in this status
|
||||||
Attachments []*MediaAttachment `pg:"-"`
|
GTSMediaAttachments []*MediaAttachment `pg:"-"`
|
||||||
// Status being replied to
|
// Status being replied to
|
||||||
ReplyToStatus *Status `pg:"-"`
|
GTSReplyToStatus *Status `pg:"-"`
|
||||||
// Account being replied to
|
// Account being replied to
|
||||||
ReplyToAccount *Account `pg:"-"`
|
GTSReplyToAccount *Account `pg:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Visibility represents the visibility granularity of a status.
|
// Visibility represents the visibility granularity of a status.
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ type MediaHandler interface {
|
||||||
// ProcessAttachment takes a new attachment and the requesting account, checks it out, removes exif data from it,
|
// ProcessAttachment takes a new attachment and the requesting account, checks it out, removes exif data from it,
|
||||||
// puts it in whatever storage backend we're using, sets the relevant fields in the database for the new media,
|
// puts it in whatever storage backend we're using, sets the relevant fields in the database for the new media,
|
||||||
// and then returns information to the caller about the attachment.
|
// and then returns information to the caller about the attachment.
|
||||||
ProcessAttachment(img []byte, accountID string) (*gtsmodel.MediaAttachment, error)
|
ProcessAttachment(attachment []byte, accountID string) (*gtsmodel.MediaAttachment, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type mediaHandler struct {
|
type mediaHandler struct {
|
||||||
|
|
@ -73,15 +73,15 @@ func New(config *config.Config, database db.DB, storage storage.Storage, log *lo
|
||||||
INTERFACE FUNCTIONS
|
INTERFACE FUNCTIONS
|
||||||
*/
|
*/
|
||||||
|
|
||||||
func (mh *mediaHandler) SetHeaderOrAvatarForAccountID(img []byte, accountID string, headerOrAvi string) (*gtsmodel.MediaAttachment, error) {
|
func (mh *mediaHandler) SetHeaderOrAvatarForAccountID(attachment []byte, accountID string, headerOrAvi string) (*gtsmodel.MediaAttachment, error) {
|
||||||
l := mh.log.WithField("func", "SetHeaderForAccountID")
|
l := mh.log.WithField("func", "SetHeaderForAccountID")
|
||||||
|
|
||||||
if headerOrAvi != MediaHeader && headerOrAvi != MediaAvatar {
|
if headerOrAvi != MediaHeader && headerOrAvi != MediaAvatar {
|
||||||
return nil, errors.New("header or avatar not selected")
|
return nil, errors.New("header or avatar not selected")
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure we have an image we can handle
|
// make sure we have a type we can handle
|
||||||
contentType, err := parseContentType(img)
|
contentType, err := parseContentType(attachment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -89,13 +89,13 @@ func (mh *mediaHandler) SetHeaderOrAvatarForAccountID(img []byte, accountID stri
|
||||||
return nil, fmt.Errorf("%s is not an accepted image type", contentType)
|
return nil, fmt.Errorf("%s is not an accepted image type", contentType)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(img) == 0 {
|
if len(attachment) == 0 {
|
||||||
return nil, fmt.Errorf("passed reader was of size 0")
|
return nil, fmt.Errorf("passed reader was of size 0")
|
||||||
}
|
}
|
||||||
l.Tracef("read %d bytes of file", len(img))
|
l.Tracef("read %d bytes of file", len(attachment))
|
||||||
|
|
||||||
// process it
|
// process it
|
||||||
ma, err := mh.processHeaderOrAvi(img, contentType, headerOrAvi, accountID)
|
ma, err := mh.processHeaderOrAvi(attachment, contentType, headerOrAvi, accountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error processing %s: %s", headerOrAvi, err)
|
return nil, fmt.Errorf("error processing %s: %s", headerOrAvi, err)
|
||||||
}
|
}
|
||||||
|
|
@ -108,8 +108,8 @@ func (mh *mediaHandler) SetHeaderOrAvatarForAccountID(img []byte, accountID stri
|
||||||
return ma, nil
|
return ma, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mh *mediaHandler) ProcessAttachment(data []byte, accountID string) (*gtsmodel.MediaAttachment, error) {
|
func (mh *mediaHandler) ProcessAttachment(attachment []byte, accountID string) (*gtsmodel.MediaAttachment, error) {
|
||||||
contentType, err := parseContentType(data)
|
contentType, err := parseContentType(attachment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -119,24 +119,24 @@ func (mh *mediaHandler) ProcessAttachment(data []byte, accountID string) (*gtsmo
|
||||||
if !supportedVideoType(contentType) {
|
if !supportedVideoType(contentType) {
|
||||||
return nil, fmt.Errorf("video type %s not supported", contentType)
|
return nil, fmt.Errorf("video type %s not supported", contentType)
|
||||||
}
|
}
|
||||||
if len(data) == 0 {
|
if len(attachment) == 0 {
|
||||||
return nil, errors.New("video was of size 0")
|
return nil, errors.New("video was of size 0")
|
||||||
}
|
}
|
||||||
if len(data) > mh.config.MediaConfig.MaxVideoSize {
|
if len(attachment) > mh.config.MediaConfig.MaxVideoSize {
|
||||||
return nil, fmt.Errorf("video size %d bytes exceeded max video size of %d bytes", len(data), mh.config.MediaConfig.MaxVideoSize)
|
return nil, fmt.Errorf("video size %d bytes exceeded max video size of %d bytes", len(attachment), mh.config.MediaConfig.MaxVideoSize)
|
||||||
}
|
}
|
||||||
return mh.processVideo(data, accountID, contentType)
|
return mh.processVideo(attachment, accountID, contentType)
|
||||||
case "image":
|
case "image":
|
||||||
if !supportedImageType(contentType) {
|
if !supportedImageType(contentType) {
|
||||||
return nil, fmt.Errorf("image type %s not supported", contentType)
|
return nil, fmt.Errorf("image type %s not supported", contentType)
|
||||||
}
|
}
|
||||||
if len(data) == 0 {
|
if len(attachment) == 0 {
|
||||||
return nil, errors.New("image was of size 0")
|
return nil, errors.New("image was of size 0")
|
||||||
}
|
}
|
||||||
if len(data) > mh.config.MediaConfig.MaxImageSize {
|
if len(attachment) > mh.config.MediaConfig.MaxImageSize {
|
||||||
return nil, fmt.Errorf("image size %d bytes exceeded max image size of %d bytes", len(data), mh.config.MediaConfig.MaxImageSize)
|
return nil, fmt.Errorf("image size %d bytes exceeded max image size of %d bytes", len(attachment), mh.config.MediaConfig.MaxImageSize)
|
||||||
}
|
}
|
||||||
return mh.processImage(data, accountID, contentType)
|
return mh.processImage(attachment, accountID, contentType)
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
@ -154,28 +154,29 @@ func (mh *mediaHandler) processVideo(data []byte, accountID string, contentType
|
||||||
func (mh *mediaHandler) processImage(data []byte, accountID string, contentType string) (*gtsmodel.MediaAttachment, error) {
|
func (mh *mediaHandler) processImage(data []byte, accountID string, contentType string) (*gtsmodel.MediaAttachment, error) {
|
||||||
var clean []byte
|
var clean []byte
|
||||||
var err error
|
var err error
|
||||||
|
var original *imageAndMeta
|
||||||
|
var small *imageAndMeta
|
||||||
|
|
||||||
switch contentType {
|
switch contentType {
|
||||||
case "image/jpeg":
|
case "image/jpeg", "image/png":
|
||||||
if clean, err = purgeExif(data); err != nil {
|
if clean, err = purgeExif(data); err != nil {
|
||||||
return nil, fmt.Errorf("error cleaning exif data: %s", err)
|
return nil, fmt.Errorf("error cleaning exif data: %s", err)
|
||||||
}
|
}
|
||||||
case "image/png":
|
original, err = deriveImage(clean, contentType)
|
||||||
if clean, err = purgeExif(data); err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error cleaning exif data: %s", err)
|
return nil, fmt.Errorf("error parsing image: %s", err)
|
||||||
}
|
}
|
||||||
case "image/gif":
|
case "image/gif":
|
||||||
clean = data
|
clean = data
|
||||||
|
original, err = deriveGif(clean, contentType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing gif: %s", err)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return nil, errors.New("media type unrecognized")
|
return nil, errors.New("media type unrecognized")
|
||||||
}
|
}
|
||||||
|
|
||||||
original, err := deriveImage(clean, contentType)
|
small, err = deriveThumbnail(clean, contentType)
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error parsing image: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
small, err := deriveThumbnail(clean, contentType)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error deriving thumbnail: %s", err)
|
return nil, fmt.Errorf("error deriving thumbnail: %s", err)
|
||||||
}
|
}
|
||||||
|
|
@ -186,7 +187,7 @@ func (mh *mediaHandler) processImage(data []byte, accountID string, contentType
|
||||||
|
|
||||||
URLbase := fmt.Sprintf("%s://%s%s", mh.config.StorageConfig.ServeProtocol, mh.config.StorageConfig.ServeHost, mh.config.StorageConfig.ServeBasePath)
|
URLbase := fmt.Sprintf("%s://%s%s", mh.config.StorageConfig.ServeProtocol, mh.config.StorageConfig.ServeHost, mh.config.StorageConfig.ServeBasePath)
|
||||||
originalURL := fmt.Sprintf("%s/%s/attachment/original/%s.%s", URLbase, accountID, newMediaID, extension)
|
originalURL := fmt.Sprintf("%s/%s/attachment/original/%s.%s", URLbase, accountID, newMediaID, extension)
|
||||||
smallURL := fmt.Sprintf("%s/%s/attachment/small/%s.%s", URLbase, accountID, newMediaID, extension)
|
smallURL := fmt.Sprintf("%s/%s/attachment/small/%s.jpeg", URLbase, accountID, newMediaID) // all thumbnails/smalls are encoded as jpeg
|
||||||
|
|
||||||
// we store the original...
|
// we store the original...
|
||||||
originalPath := fmt.Sprintf("%s/%s/%s/%s/%s.%s", mh.config.StorageConfig.BasePath, accountID, MediaAttachment, MediaOriginal, newMediaID, extension)
|
originalPath := fmt.Sprintf("%s/%s/%s/%s/%s.%s", mh.config.StorageConfig.BasePath, accountID, MediaAttachment, MediaOriginal, newMediaID, extension)
|
||||||
|
|
@ -195,7 +196,7 @@ func (mh *mediaHandler) processImage(data []byte, accountID string, contentType
|
||||||
}
|
}
|
||||||
|
|
||||||
// and a thumbnail...
|
// and a thumbnail...
|
||||||
smallPath := fmt.Sprintf("%s/%s/%s/%s/%s.%s", mh.config.StorageConfig.BasePath, accountID, MediaAttachment, MediaSmall, newMediaID, extension)
|
smallPath := fmt.Sprintf("%s/%s/%s/%s/%s.jpeg", mh.config.StorageConfig.BasePath, accountID, MediaAttachment, MediaSmall, newMediaID) // all thumbnails/smalls are encoded as jpeg
|
||||||
if err := mh.storage.StoreFileAt(smallPath, small.image); err != nil {
|
if err := mh.storage.StoreFileAt(smallPath, small.image); err != nil {
|
||||||
return nil, fmt.Errorf("storage error: %s", err)
|
return nil, fmt.Errorf("storage error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
@ -235,7 +236,7 @@ func (mh *mediaHandler) processImage(data []byte, accountID string, contentType
|
||||||
},
|
},
|
||||||
Thumbnail: gtsmodel.Thumbnail{
|
Thumbnail: gtsmodel.Thumbnail{
|
||||||
Path: smallPath,
|
Path: smallPath,
|
||||||
ContentType: contentType,
|
ContentType: "image/jpeg", // all thumbnails/smalls are encoded as jpeg
|
||||||
FileSize: len(small.image),
|
FileSize: len(small.image),
|
||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now(),
|
||||||
URL: smallURL,
|
URL: smallURL,
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,45 @@ func purgeExif(b []byte) ([]byte, error) {
|
||||||
return clean, nil
|
return clean, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func deriveGif(b []byte, extension string) (*imageAndMeta, error) {
|
||||||
|
var g *gif.GIF
|
||||||
|
var err error
|
||||||
|
switch extension {
|
||||||
|
case "image/gif":
|
||||||
|
g, err = gif.DecodeAll(bytes.NewReader(b))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("extension %s not recognised", extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
// use the first frame to get the static characteristics
|
||||||
|
width := g.Config.Width
|
||||||
|
height := g.Config.Height
|
||||||
|
size := width * height
|
||||||
|
aspect := float64(width) / float64(height)
|
||||||
|
|
||||||
|
bh, err := blurhash.Encode(4, 3, g.Image[0])
|
||||||
|
if err != nil || bh == "" {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
out := &bytes.Buffer{}
|
||||||
|
if err := gif.EncodeAll(out, g); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &imageAndMeta{
|
||||||
|
image: out.Bytes(),
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
size: size,
|
||||||
|
aspect: aspect,
|
||||||
|
blurhash: bh,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func deriveImage(b []byte, extension string) (*imageAndMeta, error) {
|
func deriveImage(b []byte, extension string) (*imageAndMeta, error) {
|
||||||
var i image.Image
|
var i image.Image
|
||||||
var err error
|
var err error
|
||||||
|
|
@ -118,11 +157,6 @@ func deriveImage(b []byte, extension string) (*imageAndMeta, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
case "image/gif":
|
|
||||||
i, err = gif.Decode(bytes.NewReader(b))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("extension %s not recognised", extension)
|
return nil, fmt.Errorf("extension %s not recognised", extension)
|
||||||
}
|
}
|
||||||
|
|
@ -131,15 +165,17 @@ func deriveImage(b []byte, extension string) (*imageAndMeta, error) {
|
||||||
height := i.Bounds().Size().Y
|
height := i.Bounds().Size().Y
|
||||||
size := width * height
|
size := width * height
|
||||||
aspect := float64(width) / float64(height)
|
aspect := float64(width) / float64(height)
|
||||||
|
|
||||||
bh, err := blurhash.Encode(4, 3, i)
|
bh, err := blurhash.Encode(4, 3, i)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error generating blurhash: %s", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
out := &bytes.Buffer{}
|
out := &bytes.Buffer{}
|
||||||
if err := jpeg.Encode(out, i, nil); err != nil {
|
if err := jpeg.Encode(out, i, nil); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &imageAndMeta{
|
return &imageAndMeta{
|
||||||
image: out.Bytes(),
|
image: out.Bytes(),
|
||||||
width: width,
|
width: width,
|
||||||
|
|
|
||||||
BIN
testrig/media/ohyou-original.jpeg
Executable file
BIN
testrig/media/ohyou-original.jpeg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
BIN
testrig/media/ohyou-small.jpeg
Executable file
BIN
testrig/media/ohyou-small.jpeg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 6 KiB |
BIN
testrig/media/ohyou.jpeg
Normal file
BIN
testrig/media/ohyou.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
BIN
testrig/media/trent-original.gif
Executable file
BIN
testrig/media/trent-original.gif
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
testrig/media/trent-small.jpeg
Executable file
BIN
testrig/media/trent-small.jpeg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
BIN
testrig/media/trent-unprocessed.gif
Normal file
BIN
testrig/media/trent-unprocessed.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1 MiB |
|
|
@ -21,7 +21,6 @@ package testrig
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/storage"
|
"github.com/superseriousbusiness/gotosocial/internal/storage"
|
||||||
)
|
)
|
||||||
|
|
@ -39,13 +38,13 @@ func NewTestStorage() storage.Storage {
|
||||||
func StandardStorageSetup(s storage.Storage, relativePath string) {
|
func StandardStorageSetup(s storage.Storage, relativePath string) {
|
||||||
stored := NewTestStored()
|
stored := NewTestStored()
|
||||||
a := NewTestAttachments()
|
a := NewTestAttachments()
|
||||||
for k, fileNameTemplate := range stored {
|
for k, paths := range stored {
|
||||||
attachmentInfo, ok := a[k]
|
attachmentInfo, ok := a[k]
|
||||||
if !ok {
|
if !ok {
|
||||||
panic(fmt.Errorf("key %s not found in test attachments", k))
|
panic(fmt.Errorf("key %s not found in test attachments", k))
|
||||||
}
|
}
|
||||||
filenameOriginal := strings.Replace(fileNameTemplate, "*", "original", 1)
|
filenameOriginal := paths.original
|
||||||
filenameSmall := strings.Replace(fileNameTemplate, "*", "small", 1)
|
filenameSmall := paths.small
|
||||||
pathOriginal := attachmentInfo.File.Path
|
pathOriginal := attachmentInfo.File.Path
|
||||||
pathSmall := attachmentInfo.Thumbnail.Path
|
pathSmall := attachmentInfo.Thumbnail.Path
|
||||||
bOriginal, err := os.ReadFile(fmt.Sprintf("%s/%s", relativePath, filenameOriginal))
|
bOriginal, err := os.ReadFile(fmt.Sprintf("%s/%s", relativePath, filenameOriginal))
|
||||||
|
|
|
||||||
|
|
@ -511,13 +511,125 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||||
Avatar: false,
|
Avatar: false,
|
||||||
Header: false,
|
Header: false,
|
||||||
},
|
},
|
||||||
|
"local_account_1_status_4_attachment_1": {
|
||||||
|
ID: "510f6033-798b-4390-81b1-c38ca2205ad3",
|
||||||
|
StatusID: "18524c05-97dc-46d7-b474-c811bd9e1e32",
|
||||||
|
URL: "http://localhost:8080/fileserver/580072df-4d03-4684-a412-89fd6f7d77e6/attachment/original/510f6033-798b-4390-81b1-c38ca2205ad3.gif",
|
||||||
|
RemoteURL: "",
|
||||||
|
CreatedAt: time.Now().Add(-1 * time.Hour),
|
||||||
|
UpdatedAt: time.Now().Add(-1 * time.Hour),
|
||||||
|
Type: gtsmodel.FileTypeGif,
|
||||||
|
FileMeta: gtsmodel.FileMeta{
|
||||||
|
Original: gtsmodel.Original{
|
||||||
|
Width: 400,
|
||||||
|
Height: 280,
|
||||||
|
Size: 756000,
|
||||||
|
Aspect: 1.4285714285714286,
|
||||||
|
},
|
||||||
|
Small: gtsmodel.Small{
|
||||||
|
Width: 256,
|
||||||
|
Height: 179,
|
||||||
|
Size: 45824,
|
||||||
|
Aspect: 1.4301675977653632,
|
||||||
|
},
|
||||||
|
Focus: gtsmodel.Focus{
|
||||||
|
X: 0,
|
||||||
|
Y: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AccountID: "580072df-4d03-4684-a412-89fd6f7d77e6",
|
||||||
|
Description: "90's Trent Reznor turning to the camera",
|
||||||
|
ScheduledStatusID: "",
|
||||||
|
Blurhash: "LEDara58O=t5EMSOENEN9]}?aK%0",
|
||||||
|
Processing: 2,
|
||||||
|
File: gtsmodel.File{
|
||||||
|
Path: "/gotosocial/storage/580072df-4d03-4684-a412-89fd6f7d77e6/attachment/original/510f6033-798b-4390-81b1-c38ca2205ad3.gif",
|
||||||
|
ContentType: "image/gif",
|
||||||
|
FileSize: 1109138,
|
||||||
|
UpdatedAt: time.Now().Add(-1 * time.Hour),
|
||||||
|
},
|
||||||
|
Thumbnail: gtsmodel.Thumbnail{
|
||||||
|
Path: "/gotosocial/storage/580072df-4d03-4684-a412-89fd6f7d77e6/attachment/small/510f6033-798b-4390-81b1-c38ca2205ad3.jpeg",
|
||||||
|
ContentType: "image/jpeg",
|
||||||
|
FileSize: 8803,
|
||||||
|
UpdatedAt: time.Now().Add(-1 * time.Hour),
|
||||||
|
URL: "http://localhost:8080/fileserver/580072df-4d03-4684-a412-89fd6f7d77e6/attachment/small/510f6033-798b-4390-81b1-c38ca2205ad3.jpeg",
|
||||||
|
RemoteURL: "",
|
||||||
|
},
|
||||||
|
Avatar: false,
|
||||||
|
Header: false,
|
||||||
|
},
|
||||||
|
"local_account_1_unattached_1": {
|
||||||
|
ID: "7a3b9f77-ab30-461e-bdd8-e64bd1db3008",
|
||||||
|
StatusID: "", // this attachment isn't connected to a status YET
|
||||||
|
URL: "http://localhost:8080/fileserver/580072df-4d03-4684-a412-89fd6f7d77e6/attachment/original/7a3b9f77-ab30-461e-bdd8-e64bd1db3008.jpeg",
|
||||||
|
RemoteURL: "",
|
||||||
|
CreatedAt: time.Now().Add(30 * time.Second),
|
||||||
|
UpdatedAt: time.Now().Add(30 * time.Second),
|
||||||
|
Type: gtsmodel.FileTypeGif,
|
||||||
|
FileMeta: gtsmodel.FileMeta{
|
||||||
|
Original: gtsmodel.Original{
|
||||||
|
Width: 800,
|
||||||
|
Height: 450,
|
||||||
|
Size: 360000,
|
||||||
|
Aspect: 1.7777777777777777,
|
||||||
|
},
|
||||||
|
Small: gtsmodel.Small{
|
||||||
|
Width: 256,
|
||||||
|
Height: 144,
|
||||||
|
Size: 36864,
|
||||||
|
Aspect: 1.7777777777777777,
|
||||||
|
},
|
||||||
|
Focus: gtsmodel.Focus{
|
||||||
|
X: 0,
|
||||||
|
Y: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AccountID: "580072df-4d03-4684-a412-89fd6f7d77e6",
|
||||||
|
Description: "the oh you meme",
|
||||||
|
ScheduledStatusID: "",
|
||||||
|
Blurhash: "LSAd]9ogDge-R:M|j=xWIto0xXWX",
|
||||||
|
Processing: 2,
|
||||||
|
File: gtsmodel.File{
|
||||||
|
Path: "/gotosocial/storage/580072df-4d03-4684-a412-89fd6f7d77e6/attachment/original/7a3b9f77-ab30-461e-bdd8-e64bd1db3008.jpeg",
|
||||||
|
ContentType: "image/jpeg",
|
||||||
|
FileSize: 27759,
|
||||||
|
UpdatedAt: time.Now().Add(30 * time.Second),
|
||||||
|
},
|
||||||
|
Thumbnail: gtsmodel.Thumbnail{
|
||||||
|
Path: "/gotosocial/storage/580072df-4d03-4684-a412-89fd6f7d77e6/attachment/small/7a3b9f77-ab30-461e-bdd8-e64bd1db3008.jpeg",
|
||||||
|
ContentType: "image/jpeg",
|
||||||
|
FileSize: 6177,
|
||||||
|
UpdatedAt: time.Now().Add(30 * time.Second),
|
||||||
|
URL: "http://localhost:8080/fileserver/580072df-4d03-4684-a412-89fd6f7d77e6/attachment/small/7a3b9f77-ab30-461e-bdd8-e64bd1db3008.jpeg",
|
||||||
|
RemoteURL: "",
|
||||||
|
},
|
||||||
|
Avatar: false,
|
||||||
|
Header: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type paths struct {
|
||||||
|
original string
|
||||||
|
small string
|
||||||
|
}
|
||||||
|
|
||||||
// NewTestStored returns a map of filenames, keyed according to which attachment they pertain to.
|
// NewTestStored returns a map of filenames, keyed according to which attachment they pertain to.
|
||||||
func NewTestStored() map[string]string {
|
func NewTestStored() map[string]paths {
|
||||||
return map[string]string{
|
return map[string]paths{
|
||||||
"admin_account_status_1_attachment_1": "welcome-*.jpeg",
|
"admin_account_status_1_attachment_1": {
|
||||||
|
original: "welcome-original.jpeg",
|
||||||
|
small: "welcome-small.jpeg",
|
||||||
|
},
|
||||||
|
"local_account_1_status_4_attachment_1": {
|
||||||
|
original: "trent-original.gif",
|
||||||
|
small: "trent-small.jpeg",
|
||||||
|
},
|
||||||
|
"local_account_1_unattached_1": {
|
||||||
|
original: "ohyou-original.jpeg",
|
||||||
|
small: "ohyou-small.jpeg",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -530,6 +642,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
URI: "http://localhost:8080/users/admin/statuses/502ccd6f-0edf-48d7-9016-2dfa4d3714cd",
|
URI: "http://localhost:8080/users/admin/statuses/502ccd6f-0edf-48d7-9016-2dfa4d3714cd",
|
||||||
URL: "http://localhost:8080/@admin/statuses/502ccd6f-0edf-48d7-9016-2dfa4d3714cd",
|
URL: "http://localhost:8080/@admin/statuses/502ccd6f-0edf-48d7-9016-2dfa4d3714cd",
|
||||||
Content: "hello world! first post on the instance!",
|
Content: "hello world! first post on the instance!",
|
||||||
|
Attachments: []string{"b052241b-f30f-4dc6-92fc-2bad0be1f8d8"},
|
||||||
CreatedAt: time.Now().Add(-71 * time.Hour),
|
CreatedAt: time.Now().Add(-71 * time.Hour),
|
||||||
UpdatedAt: time.Now().Add(-71 * time.Hour),
|
UpdatedAt: time.Now().Add(-71 * time.Hour),
|
||||||
Local: true,
|
Local: true,
|
||||||
|
|
@ -640,6 +753,30 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
},
|
},
|
||||||
ActivityStreamsType: gtsmodel.ActivityStreamsNote,
|
ActivityStreamsType: gtsmodel.ActivityStreamsNote,
|
||||||
},
|
},
|
||||||
|
"local_account_1_status_4": {
|
||||||
|
ID: "18524c05-97dc-46d7-b474-c811bd9e1e32",
|
||||||
|
URI: "http://localhost:8080/users/the_mighty_zork/statuses/18524c05-97dc-46d7-b474-c811bd9e1e32",
|
||||||
|
URL: "http://localhost:8080/@the_mighty_zork/statuses/18524c05-97dc-46d7-b474-c811bd9e1e32",
|
||||||
|
Content: "here's a little gif of trent",
|
||||||
|
Attachments: []string{"510f6033-798b-4390-81b1-c38ca2205ad3"},
|
||||||
|
CreatedAt: time.Now().Add(-1 * time.Hour),
|
||||||
|
UpdatedAt: time.Now().Add(-1 * time.Hour),
|
||||||
|
Local: true,
|
||||||
|
AccountID: "580072df-4d03-4684-a412-89fd6f7d77e6",
|
||||||
|
InReplyToID: "",
|
||||||
|
BoostOfID: "",
|
||||||
|
ContentWarning: "eye contact, trent reznor gif",
|
||||||
|
Visibility: gtsmodel.VisibilityMutualsOnly,
|
||||||
|
Sensitive: false,
|
||||||
|
Language: "en",
|
||||||
|
VisibilityAdvanced: >smodel.VisibilityAdvanced{
|
||||||
|
Federated: true,
|
||||||
|
Boostable: true,
|
||||||
|
Replyable: true,
|
||||||
|
Likeable: true,
|
||||||
|
},
|
||||||
|
ActivityStreamsType: gtsmodel.ActivityStreamsNote,
|
||||||
|
},
|
||||||
"local_account_2_status_1": {
|
"local_account_2_status_1": {
|
||||||
ID: "8945ccf2-3873-45e9-aa13-fd7163f19775",
|
ID: "8945ccf2-3873-45e9-aa13-fd7163f19775",
|
||||||
URI: "http://localhost:8080/users/1happyturtle/statuses/8945ccf2-3873-45e9-aa13-fd7163f19775",
|
URI: "http://localhost:8080/users/1happyturtle/statuses/8945ccf2-3873-45e9-aa13-fd7163f19775",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue