mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-11-03 23:32:24 -06:00 
			
		
		
		
	[chore] media and emoji refactoring (#3000)
* start updating media manager interface ready for storing attachments / emoji right away
* store emoji and media as uncached immediately, then (re-)cache on Processing{}.Load()
* remove now unused media workers
* fix tests and issues
* fix another test!
* fix emoji activitypub uri setting behaviour, fix remainder of test compilation issues
* fix more tests
* fix (most of) remaining tests, add debouncing to repeatedly failing media / emojis
* whoops, rebase issue
* remove kim's whacky experiments
* do some reshuffling, ensure emoji uri gets set
* ensure marked as not cached on cleanup
* tweaks to media / emoji processing to handle context canceled better
* ensure newly fetched emojis actually get set in returned slice
* use different varnames to be a bit more obvious
* move emoji refresh rate limiting to dereferencer
* add exported dereferencer functions for remote media, use these for recaching in processor
* add check for nil attachment in updateAttachment()
* remove unused emoji and media fields + columns
* see previous commit
* fix old migrations expecting image_updated_at to exists (from copies of old models)
* remove freshness checking code (seems to be broken...)
* fix error arg causing nil ptr exception
* finish documentating functions with comments, slight tweaks to media / emoji deref error logic
* remove some extra unneeded boolean checking
* finish writing documentation (code comments) for exported media manager methods
* undo changes to migration snapshot gtsmodels, updated failing migration to have its own snapshot
* move doesColumnExist() to util.go in migrations package
	
	
This commit is contained in:
		
					parent
					
						
							
								fa710057c8
							
						
					
				
			
			
				commit
				
					
						21bb324156
					
				
			
		
					 48 changed files with 2578 additions and 1926 deletions
				
			
		| 
						 | 
					@ -376,7 +376,17 @@ func (suite *InboxPostTestSuite) TestPostUpdate() {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// emojis should be updated
 | 
						// emojis should be updated
 | 
				
			||||||
	suite.Contains(dbUpdatedAccount.EmojiIDs, testEmoji.ID)
 | 
						var haveUpdatedEmoji bool
 | 
				
			||||||
 | 
						for _, emoji := range dbUpdatedAccount.Emojis {
 | 
				
			||||||
 | 
							if emoji.Shortcode == testEmoji.Shortcode &&
 | 
				
			||||||
 | 
								emoji.Domain == testEmoji.Domain &&
 | 
				
			||||||
 | 
								emoji.ImageRemoteURL == emoji.ImageRemoteURL &&
 | 
				
			||||||
 | 
								emoji.ImageStaticRemoteURL == emoji.ImageStaticRemoteURL {
 | 
				
			||||||
 | 
								haveUpdatedEmoji = true
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						suite.True(haveUpdatedEmoji)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// account should be freshly fetched
 | 
						// account should be freshly fetched
 | 
				
			||||||
	suite.WithinDuration(time.Now(), dbUpdatedAccount.FetchedAt, 10*time.Second)
 | 
						suite.WithinDuration(time.Now(), dbUpdatedAccount.FetchedAt, 10*time.Second)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -281,7 +281,7 @@ func (suite *EmojiCreateTestSuite) TestEmojiCreateAlreadyExists() {
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotEmpty(b)
 | 
						suite.NotEmpty(b)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	suite.Equal(`{"error":"Conflict: emoji with shortcode rainbow already exists"}`, string(b))
 | 
						suite.Equal(`{"error":"Conflict: emoji with shortcode already exists"}`, string(b))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestEmojiCreateTestSuite(t *testing.T) {
 | 
					func TestEmojiCreateTestSuite(t *testing.T) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,6 +20,7 @@ package admin_test
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
	"io/ioutil"
 | 
						"io/ioutil"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"net/http/httptest"
 | 
						"net/http/httptest"
 | 
				
			||||||
| 
						 | 
					@ -370,10 +371,10 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateModifyRemoteEmoji() {
 | 
				
			||||||
	defer result.Body.Close()
 | 
						defer result.Body.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// check the response
 | 
						// check the response
 | 
				
			||||||
	b, err := ioutil.ReadAll(result.Body)
 | 
						b, err := io.ReadAll(result.Body)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	suite.Equal(`{"error":"Bad Request: emoji 01GD5KP5CQEE1R3X43Y1EHS2CW is not a local emoji, cannot update it via this endpoint"}`, string(b))
 | 
						suite.Equal(`{"error":"Bad Request: cannot modify remote emoji"}`, string(b))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (suite *EmojiUpdateTestSuite) TestEmojiUpdateModifyNoParams() {
 | 
					func (suite *EmojiUpdateTestSuite) TestEmojiUpdateModifyNoParams() {
 | 
				
			||||||
| 
						 | 
					@ -440,7 +441,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyLocalToLocal() {
 | 
				
			||||||
	b, err := ioutil.ReadAll(result.Body)
 | 
						b, err := ioutil.ReadAll(result.Body)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	suite.Equal(`{"error":"Bad Request: emoji 01F8MH9H8E4VG3KDYJR9EGPXCQ is not a remote emoji, cannot copy it to local"}`, string(b))
 | 
						suite.Equal(`{"error":"Bad Request: target emoji is not remote; cannot copy to local"}`, string(b))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyEmptyShortcode() {
 | 
					func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyEmptyShortcode() {
 | 
				
			||||||
| 
						 | 
					@ -541,7 +542,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyShortcodeAlreadyInUse() {
 | 
				
			||||||
	b, err := ioutil.ReadAll(result.Body)
 | 
						b, err := ioutil.ReadAll(result.Body)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	suite.Equal(`{"error":"Conflict: emoji with shortcode rainbow already exists on this instance"}`, string(b))
 | 
						suite.Equal(`{"error":"Conflict: emoji with shortcode already exists"}`, string(b))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestEmojiUpdateTestSuite(t *testing.T) {
 | 
					func TestEmojiUpdateTestSuite(t *testing.T) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,7 +19,7 @@ package fileserver_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"io/ioutil"
 | 
						"io"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"net/http/httptest"
 | 
						"net/http/httptest"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
| 
						 | 
					@ -28,6 +28,7 @@ import (
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/api/fileserver"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/api/fileserver"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/media"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/media"
 | 
				
			||||||
 | 
						"github.com/superseriousbusiness/gotosocial/internal/middleware"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/testrig"
 | 
						"github.com/superseriousbusiness/gotosocial/testrig"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -54,12 +55,15 @@ func (suite *ServeFileTestSuite) GetFile(
 | 
				
			||||||
	ctx.AddParam(fileserver.MediaSizeKey, string(mediaSize))
 | 
						ctx.AddParam(fileserver.MediaSizeKey, string(mediaSize))
 | 
				
			||||||
	ctx.AddParam(fileserver.FileNameKey, filename)
 | 
						ctx.AddParam(fileserver.FileNameKey, filename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						logger := middleware.Logger(false)
 | 
				
			||||||
	suite.fileServer.ServeFile(ctx)
 | 
						suite.fileServer.ServeFile(ctx)
 | 
				
			||||||
 | 
						logger(ctx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	code = recorder.Code
 | 
						code = recorder.Code
 | 
				
			||||||
	headers = recorder.Result().Header
 | 
						headers = recorder.Result().Header
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var err error
 | 
						var err error
 | 
				
			||||||
	body, err = ioutil.ReadAll(recorder.Body)
 | 
						body, err = io.ReadAll(recorder.Body)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		suite.FailNow(err.Error())
 | 
							suite.FailNow(err.Error())
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										3
									
								
								internal/cache/size.go
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								internal/cache/size.go
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -334,7 +334,6 @@ func sizeofEmoji() uintptr {
 | 
				
			||||||
		ImageStaticPath:        exampleURI,
 | 
							ImageStaticPath:        exampleURI,
 | 
				
			||||||
		ImageContentType:       "image/png",
 | 
							ImageContentType:       "image/png",
 | 
				
			||||||
		ImageStaticContentType: "image/png",
 | 
							ImageStaticContentType: "image/png",
 | 
				
			||||||
		ImageUpdatedAt:         exampleTime,
 | 
					 | 
				
			||||||
		Disabled:               func() *bool { ok := false; return &ok }(),
 | 
							Disabled:               func() *bool { ok := false; return &ok }(),
 | 
				
			||||||
		URI:                    "http://localhost:8080/emoji/01F8MH9H8E4VG3KDYJR9EGPXCQ",
 | 
							URI:                    "http://localhost:8080/emoji/01F8MH9H8E4VG3KDYJR9EGPXCQ",
 | 
				
			||||||
		VisibleInPicker:        func() *bool { ok := true; return &ok }(),
 | 
							VisibleInPicker:        func() *bool { ok := true; return &ok }(),
 | 
				
			||||||
| 
						 | 
					@ -473,12 +472,10 @@ func sizeofMedia() uintptr {
 | 
				
			||||||
		File: gtsmodel.File{
 | 
							File: gtsmodel.File{
 | 
				
			||||||
			Path:        exampleURI,
 | 
								Path:        exampleURI,
 | 
				
			||||||
			ContentType: "image/jpeg",
 | 
								ContentType: "image/jpeg",
 | 
				
			||||||
			UpdatedAt:   exampleTime,
 | 
					 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		Thumbnail: gtsmodel.Thumbnail{
 | 
							Thumbnail: gtsmodel.Thumbnail{
 | 
				
			||||||
			Path:        exampleURI,
 | 
								Path:        exampleURI,
 | 
				
			||||||
			ContentType: "image/jpeg",
 | 
								ContentType: "image/jpeg",
 | 
				
			||||||
			UpdatedAt:   exampleTime,
 | 
					 | 
				
			||||||
			URL:         exampleURI,
 | 
								URL:         exampleURI,
 | 
				
			||||||
			RemoteURL:   exampleURI,
 | 
								RemoteURL:   exampleURI,
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -386,11 +386,10 @@ func (suite *MediaTestSuite) TestUncacheAndRecache() {
 | 
				
			||||||
		testStatusAttachment,
 | 
							testStatusAttachment,
 | 
				
			||||||
		testHeader,
 | 
							testHeader,
 | 
				
			||||||
	} {
 | 
						} {
 | 
				
			||||||
		processingRecache, err := suite.manager.PreProcessMediaRecache(ctx, data, original.ID)
 | 
							processing := suite.manager.RecacheMedia(original, data)
 | 
				
			||||||
		suite.NoError(err)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// synchronously load the recached attachment
 | 
							// synchronously load the recached attachment
 | 
				
			||||||
		recachedAttachment, err := processingRecache.LoadAttachment(ctx)
 | 
							recachedAttachment, err := processing.Load(ctx)
 | 
				
			||||||
		suite.NoError(err)
 | 
							suite.NoError(err)
 | 
				
			||||||
		suite.NotNil(recachedAttachment)
 | 
							suite.NotNil(recachedAttachment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,7 +20,7 @@ package migrations
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 | 
						gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20230521105850_emoji_empty_domain_fix"
 | 
				
			||||||
	"github.com/uptrace/bun"
 | 
						"github.com/uptrace/bun"
 | 
				
			||||||
	"github.com/uptrace/bun/dialect"
 | 
						"github.com/uptrace/bun/dialect"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,53 @@
 | 
				
			||||||
 | 
					// GoToSocial
 | 
				
			||||||
 | 
					// Copyright (C) GoToSocial Authors admin@gotosocial.org
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: AGPL-3.0-or-later
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// 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 gtsmodel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Emoji represents a custom emoji that's been uploaded through the admin UI or downloaded from a remote instance.
 | 
				
			||||||
 | 
					type Emoji struct {
 | 
				
			||||||
 | 
						ID                     string         `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"`                                // id of this item in the database
 | 
				
			||||||
 | 
						CreatedAt              time.Time      `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`                         // when was item created
 | 
				
			||||||
 | 
						UpdatedAt              time.Time      `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`                         // when was item last updated
 | 
				
			||||||
 | 
						Shortcode              string         `validate:"required" bun:",nullzero,notnull,unique:domainshortcode"`                                     // String shortcode for this emoji -- the part that's between colons. This should be lowercase a-z_  eg., 'blob_hug' 'purple_heart' Must be unique with domain.
 | 
				
			||||||
 | 
						Domain                 string         `validate:"omitempty,fqdn" bun:",nullzero,unique:domainshortcode"`                                       // Origin domain of this emoji, eg 'example.org', 'queer.party'. empty string for local emojis.
 | 
				
			||||||
 | 
						ImageRemoteURL         string         `validate:"required_without=ImageURL,omitempty,url" bun:",nullzero"`                                     // Where can this emoji be retrieved remotely? Null for local emojis.
 | 
				
			||||||
 | 
						ImageStaticRemoteURL   string         `validate:"required_without=ImageStaticURL,omitempty,url" bun:",nullzero"`                               // Where can a static / non-animated version of this emoji be retrieved remotely? Null for local emojis.
 | 
				
			||||||
 | 
						ImageURL               string         `validate:"required_without=ImageRemoteURL,required_without=Domain,omitempty,url" bun:",nullzero"`       // Where can this emoji be retrieved from the local server? Null for remote emojis.
 | 
				
			||||||
 | 
						ImageStaticURL         string         `validate:"required_without=ImageStaticRemoteURL,required_without=Domain,omitempty,url" bun:",nullzero"` // Where can a static version of this emoji be retrieved from the local server? Null for remote emojis.
 | 
				
			||||||
 | 
						ImagePath              string         `validate:"required,file" bun:",nullzero,notnull"`                                                       // Path of the emoji image in the server storage system.
 | 
				
			||||||
 | 
						ImageStaticPath        string         `validate:"required,file" bun:",nullzero,notnull"`                                                       // Path of a static version of the emoji image in the server storage system
 | 
				
			||||||
 | 
						ImageContentType       string         `validate:"required" bun:",nullzero,notnull"`                                                            // MIME content type of the emoji image
 | 
				
			||||||
 | 
						ImageStaticContentType string         `validate:"required" bun:",nullzero,notnull"`                                                            // MIME content type of the static version of the emoji image.
 | 
				
			||||||
 | 
						ImageFileSize          int            `validate:"required,min=1" bun:",nullzero,notnull"`                                                      // Size of the emoji image file in bytes, for serving purposes.
 | 
				
			||||||
 | 
						ImageStaticFileSize    int            `validate:"required,min=1" bun:",nullzero,notnull"`                                                      // Size of the static version of the emoji image file in bytes, for serving purposes.
 | 
				
			||||||
 | 
						ImageUpdatedAt         time.Time      `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"`                         // When was the emoji image last updated?
 | 
				
			||||||
 | 
						Disabled               *bool          `validate:"-" bun:",nullzero,notnull,default:false"`                                                     // Has a moderation action disabled this emoji from being shown?
 | 
				
			||||||
 | 
						URI                    string         `validate:"url" bun:",nullzero,notnull,unique"`                                                          // ActivityPub uri of this emoji. Something like 'https://example.org/emojis/1234'
 | 
				
			||||||
 | 
						VisibleInPicker        *bool          `validate:"-" bun:",nullzero,notnull,default:true"`                                                      // Is this emoji visible in the admin emoji picker?
 | 
				
			||||||
 | 
						Category               *EmojiCategory `validate:"-" bun:"rel:belongs-to"`                                                                      // In which emoji category is this emoji visible?
 | 
				
			||||||
 | 
						CategoryID             string         `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"`                                                 // ID of the category this emoji belongs to.
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// EmojiCategory represents a grouping of custom emojis.
 | 
				
			||||||
 | 
					type EmojiCategory struct {
 | 
				
			||||||
 | 
						ID        string    `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"`        // id of this item in the database
 | 
				
			||||||
 | 
						CreatedAt time.Time `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
 | 
				
			||||||
 | 
						UpdatedAt time.Time `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
 | 
				
			||||||
 | 
						Name      string    `validate:"required" bun:",nullzero,notnull,unique"`                             // name of this category
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,68 @@
 | 
				
			||||||
 | 
					// GoToSocial
 | 
				
			||||||
 | 
					// Copyright (C) GoToSocial Authors admin@gotosocial.org
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: AGPL-3.0-or-later
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// 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 migrations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/uptrace/bun"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						up := func(ctx context.Context, db *bun.DB) error {
 | 
				
			||||||
 | 
							return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								for _, dropcase := range []struct {
 | 
				
			||||||
 | 
									table string
 | 
				
			||||||
 | 
									col   string
 | 
				
			||||||
 | 
								}{
 | 
				
			||||||
 | 
									{table: "media_attachments", col: "file_updated_at"},
 | 
				
			||||||
 | 
									{table: "media_attachments", col: "thumbnail_updated_at"},
 | 
				
			||||||
 | 
									{table: "emojis", col: "thumbnail_updated_at"},
 | 
				
			||||||
 | 
								} {
 | 
				
			||||||
 | 
									// For each case check the column actually exists on database.
 | 
				
			||||||
 | 
									exists, err := doesColumnExist(ctx, tx, dropcase.table, dropcase.col)
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										return err
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									if exists {
 | 
				
			||||||
 | 
										// Now actually drop the column.
 | 
				
			||||||
 | 
										if _, err := tx.NewDropColumn().
 | 
				
			||||||
 | 
											Table(dropcase.table).
 | 
				
			||||||
 | 
											Column(dropcase.col).
 | 
				
			||||||
 | 
											Exec(ctx); err != nil {
 | 
				
			||||||
 | 
											return err
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						down := func(ctx context.Context, db *bun.DB) error {
 | 
				
			||||||
 | 
							return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := Migrations.Register(up, down); err != nil {
 | 
				
			||||||
 | 
							panic(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										40
									
								
								internal/db/bundb/migrations/util.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								internal/db/bundb/migrations/util.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,40 @@
 | 
				
			||||||
 | 
					// GoToSocial
 | 
				
			||||||
 | 
					// Copyright (C) GoToSocial Authors admin@gotosocial.org
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: AGPL-3.0-or-later
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// 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 migrations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/uptrace/bun"
 | 
				
			||||||
 | 
						"github.com/uptrace/bun/dialect"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// doesColumnExist safely checks whether given column exists on table, handling both SQLite and PostgreSQL appropriately.
 | 
				
			||||||
 | 
					func doesColumnExist(ctx context.Context, tx bun.Tx, table, col string) (bool, error) {
 | 
				
			||||||
 | 
						var n int
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						switch tx.Dialect().Name() {
 | 
				
			||||||
 | 
						case dialect.SQLite:
 | 
				
			||||||
 | 
							err = tx.NewRaw("SELECT COUNT(*) FROM pragma_table_info(?) WHERE name=?", table, col).Scan(ctx, &n)
 | 
				
			||||||
 | 
						case dialect.PG:
 | 
				
			||||||
 | 
							err = tx.NewRaw("SELECT COUNT(*) FROM information_schema.columns WHERE table_name=? and column_name=?", table, col).Scan(ctx, &n)
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							panic("unexpected dialect")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return (n > 0), err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -33,7 +33,6 @@ import (
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/id"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/id"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/log"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/log"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/media"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/media"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/transport"
 | 
					 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/util"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/util"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -730,18 +729,18 @@ func (d *Dereferencer) enrichAccount(
 | 
				
			||||||
	latestAcc.ID = account.ID
 | 
						latestAcc.ID = account.ID
 | 
				
			||||||
	latestAcc.FetchedAt = time.Now()
 | 
						latestAcc.FetchedAt = time.Now()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Ensure the account's avatar media is populated, passing in existing to check for changes.
 | 
						// Ensure the account's avatar media is populated, passing in existing to check for chages.
 | 
				
			||||||
	if err := d.fetchRemoteAccountAvatar(ctx, tsport, account, latestAcc); err != nil {
 | 
						if err := d.fetchAccountAvatar(ctx, requestUser, account, latestAcc); err != nil {
 | 
				
			||||||
		log.Errorf(ctx, "error fetching remote avatar for account %s: %v", uri, err)
 | 
							log.Errorf(ctx, "error fetching remote avatar for account %s: %v", uri, err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Ensure the account's avatar media is populated, passing in existing to check for changes.
 | 
						// Ensure the account's avatar media is populated, passing in existing to check for chages.
 | 
				
			||||||
	if err := d.fetchRemoteAccountHeader(ctx, tsport, account, latestAcc); err != nil {
 | 
						if err := d.fetchAccountHeader(ctx, requestUser, account, latestAcc); err != nil {
 | 
				
			||||||
		log.Errorf(ctx, "error fetching remote header for account %s: %v", uri, err)
 | 
							log.Errorf(ctx, "error fetching remote header for account %s: %v", uri, err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Fetch the latest remote account emoji IDs used in account display name/bio.
 | 
						// Fetch the latest remote account emoji IDs used in account display name/bio.
 | 
				
			||||||
	if _, err = d.fetchRemoteAccountEmojis(ctx, latestAcc, requestUser); err != nil {
 | 
						if err = d.fetchAccountEmojis(ctx, account, latestAcc); err != nil {
 | 
				
			||||||
		log.Errorf(ctx, "error fetching remote emojis for account %s: %v", uri, err)
 | 
							log.Errorf(ctx, "error fetching remote emojis for account %s: %v", uri, err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -779,9 +778,9 @@ func (d *Dereferencer) enrichAccount(
 | 
				
			||||||
	return latestAcc, apubAcc, nil
 | 
						return latestAcc, apubAcc, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (d *Dereferencer) fetchRemoteAccountAvatar(
 | 
					func (d *Dereferencer) fetchAccountAvatar(
 | 
				
			||||||
	ctx context.Context,
 | 
						ctx context.Context,
 | 
				
			||||||
	tsport transport.Transport,
 | 
						requestUser string,
 | 
				
			||||||
	existingAcc *gtsmodel.Account,
 | 
						existingAcc *gtsmodel.Account,
 | 
				
			||||||
	latestAcc *gtsmodel.Account,
 | 
						latestAcc *gtsmodel.Account,
 | 
				
			||||||
) error {
 | 
					) error {
 | 
				
			||||||
| 
						 | 
					@ -808,7 +807,7 @@ func (d *Dereferencer) fetchRemoteAccountAvatar(
 | 
				
			||||||
			// Ensuring existing attachment is up-to-date
 | 
								// Ensuring existing attachment is up-to-date
 | 
				
			||||||
			// and any recaching is performed if required.
 | 
								// and any recaching is performed if required.
 | 
				
			||||||
			existing, err := d.updateAttachment(ctx,
 | 
								existing, err := d.updateAttachment(ctx,
 | 
				
			||||||
				tsport,
 | 
									requestUser,
 | 
				
			||||||
				existing,
 | 
									existing,
 | 
				
			||||||
				nil,
 | 
									nil,
 | 
				
			||||||
			)
 | 
								)
 | 
				
			||||||
| 
						 | 
					@ -830,20 +829,25 @@ func (d *Dereferencer) fetchRemoteAccountAvatar(
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Fetch newly changed avatar from remote.
 | 
						// Fetch newly changed avatar.
 | 
				
			||||||
	attachment, err := d.loadAttachment(ctx,
 | 
						attachment, err := d.GetMedia(ctx,
 | 
				
			||||||
		tsport,
 | 
							requestUser,
 | 
				
			||||||
		latestAcc.ID,
 | 
							latestAcc.ID,
 | 
				
			||||||
		latestAcc.AvatarRemoteURL,
 | 
							latestAcc.AvatarRemoteURL,
 | 
				
			||||||
		&media.AdditionalMediaInfo{
 | 
							media.AdditionalMediaInfo{
 | 
				
			||||||
			Avatar:    util.Ptr(true),
 | 
								Avatar:    util.Ptr(true),
 | 
				
			||||||
			RemoteURL: &latestAcc.AvatarRemoteURL,
 | 
								RemoteURL: &latestAcc.AvatarRemoteURL,
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if attachment == nil {
 | 
				
			||||||
			return gtserror.Newf("error loading attachment %s: %w", latestAcc.AvatarRemoteURL, err)
 | 
								return gtserror.Newf("error loading attachment %s: %w", latestAcc.AvatarRemoteURL, err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// non-fatal error occurred during loading, still use it.
 | 
				
			||||||
 | 
							log.Warnf(ctx, "partially loaded attachment: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Set the avatar attachment on account model.
 | 
						// Set the avatar attachment on account model.
 | 
				
			||||||
	latestAcc.AvatarMediaAttachment = attachment
 | 
						latestAcc.AvatarMediaAttachment = attachment
 | 
				
			||||||
	latestAcc.AvatarMediaAttachmentID = attachment.ID
 | 
						latestAcc.AvatarMediaAttachmentID = attachment.ID
 | 
				
			||||||
| 
						 | 
					@ -851,9 +855,9 @@ func (d *Dereferencer) fetchRemoteAccountAvatar(
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (d *Dereferencer) fetchRemoteAccountHeader(
 | 
					func (d *Dereferencer) fetchAccountHeader(
 | 
				
			||||||
	ctx context.Context,
 | 
						ctx context.Context,
 | 
				
			||||||
	tsport transport.Transport,
 | 
						requestUser string,
 | 
				
			||||||
	existingAcc *gtsmodel.Account,
 | 
						existingAcc *gtsmodel.Account,
 | 
				
			||||||
	latestAcc *gtsmodel.Account,
 | 
						latestAcc *gtsmodel.Account,
 | 
				
			||||||
) error {
 | 
					) error {
 | 
				
			||||||
| 
						 | 
					@ -880,7 +884,7 @@ func (d *Dereferencer) fetchRemoteAccountHeader(
 | 
				
			||||||
			// Ensuring existing attachment is up-to-date
 | 
								// Ensuring existing attachment is up-to-date
 | 
				
			||||||
			// and any recaching is performed if required.
 | 
								// and any recaching is performed if required.
 | 
				
			||||||
			existing, err := d.updateAttachment(ctx,
 | 
								existing, err := d.updateAttachment(ctx,
 | 
				
			||||||
				tsport,
 | 
									requestUser,
 | 
				
			||||||
				existing,
 | 
									existing,
 | 
				
			||||||
				nil,
 | 
									nil,
 | 
				
			||||||
			)
 | 
								)
 | 
				
			||||||
| 
						 | 
					@ -902,20 +906,25 @@ func (d *Dereferencer) fetchRemoteAccountHeader(
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Fetch newly changed header from remote.
 | 
						// Fetch newly changed header.
 | 
				
			||||||
	attachment, err := d.loadAttachment(ctx,
 | 
						attachment, err := d.GetMedia(ctx,
 | 
				
			||||||
		tsport,
 | 
							requestUser,
 | 
				
			||||||
		latestAcc.ID,
 | 
							latestAcc.ID,
 | 
				
			||||||
		latestAcc.HeaderRemoteURL,
 | 
							latestAcc.HeaderRemoteURL,
 | 
				
			||||||
		&media.AdditionalMediaInfo{
 | 
							media.AdditionalMediaInfo{
 | 
				
			||||||
			Header:    util.Ptr(true),
 | 
								Header:    util.Ptr(true),
 | 
				
			||||||
			RemoteURL: &latestAcc.HeaderRemoteURL,
 | 
								RemoteURL: &latestAcc.HeaderRemoteURL,
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if attachment == nil {
 | 
				
			||||||
			return gtserror.Newf("error loading attachment %s: %w", latestAcc.HeaderRemoteURL, err)
 | 
								return gtserror.Newf("error loading attachment %s: %w", latestAcc.HeaderRemoteURL, err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// non-fatal error occurred during loading, still use it.
 | 
				
			||||||
 | 
							log.Warnf(ctx, "partially loaded attachment: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Set the header attachment on account model.
 | 
						// Set the header attachment on account model.
 | 
				
			||||||
	latestAcc.HeaderMediaAttachment = attachment
 | 
						latestAcc.HeaderMediaAttachment = attachment
 | 
				
			||||||
	latestAcc.HeaderMediaAttachmentID = attachment.ID
 | 
						latestAcc.HeaderMediaAttachmentID = attachment.ID
 | 
				
			||||||
| 
						 | 
					@ -923,119 +932,44 @@ func (d *Dereferencer) fetchRemoteAccountHeader(
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (d *Dereferencer) fetchRemoteAccountEmojis(ctx context.Context, targetAccount *gtsmodel.Account, requestingUsername string) (bool, error) {
 | 
					func (d *Dereferencer) fetchAccountEmojis(
 | 
				
			||||||
	maybeEmojis := targetAccount.Emojis
 | 
						ctx context.Context,
 | 
				
			||||||
	maybeEmojiIDs := targetAccount.EmojiIDs
 | 
						existing *gtsmodel.Account,
 | 
				
			||||||
 | 
						account *gtsmodel.Account,
 | 
				
			||||||
	// It's possible that the account had emoji IDs set on it, but not Emojis
 | 
					) error {
 | 
				
			||||||
	// themselves, depending on how it was fetched before being passed to us.
 | 
						// Fetch the updated emojis for our account.
 | 
				
			||||||
	//
 | 
						emojis, changed, err := d.fetchEmojis(ctx,
 | 
				
			||||||
	// If we only have IDs, fetch the emojis from the db. We know they're in
 | 
							existing.Emojis,
 | 
				
			||||||
	// there or else they wouldn't have IDs.
 | 
							account.Emojis,
 | 
				
			||||||
	if len(maybeEmojiIDs) > len(maybeEmojis) {
 | 
					 | 
				
			||||||
		maybeEmojis = make([]*gtsmodel.Emoji, 0, len(maybeEmojiIDs))
 | 
					 | 
				
			||||||
		for _, emojiID := range maybeEmojiIDs {
 | 
					 | 
				
			||||||
			maybeEmoji, err := d.state.DB.GetEmojiByID(ctx, emojiID)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return false, err
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			maybeEmojis = append(maybeEmojis, maybeEmoji)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// For all the maybe emojis we have, we either fetch them from the database
 | 
					 | 
				
			||||||
	// (if we haven't already), or dereference them from the remote instance.
 | 
					 | 
				
			||||||
	gotEmojis, err := d.populateEmojis(ctx, maybeEmojis, requestingUsername)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return false, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Extract the ID of each fetched or dereferenced emoji, so we can attach
 | 
					 | 
				
			||||||
	// this to the account if necessary.
 | 
					 | 
				
			||||||
	gotEmojiIDs := make([]string, 0, len(gotEmojis))
 | 
					 | 
				
			||||||
	for _, e := range gotEmojis {
 | 
					 | 
				
			||||||
		gotEmojiIDs = append(gotEmojiIDs, e.ID)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var (
 | 
					 | 
				
			||||||
		changed  = false // have the emojis for this account changed?
 | 
					 | 
				
			||||||
		maybeLen = len(maybeEmojis)
 | 
					 | 
				
			||||||
		gotLen   = len(gotEmojis)
 | 
					 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
	// if the length of everything is zero, this is simple:
 | 
							return gtserror.Newf("error fetching emojis: %w", err)
 | 
				
			||||||
	// nothing has changed and there's nothing to do
 | 
					 | 
				
			||||||
	if maybeLen == 0 && gotLen == 0 {
 | 
					 | 
				
			||||||
		return changed, nil
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// if the *amount* of emojis on the account has changed, then the got emojis
 | 
						if !changed {
 | 
				
			||||||
	// are definitely different from the previous ones (if there were any) --
 | 
							// Use existing account emoji objects.
 | 
				
			||||||
	// the account has either more or fewer emojis set on it now, so take the
 | 
							account.EmojiIDs = existing.EmojiIDs
 | 
				
			||||||
	// discovered emojis as the new correct ones.
 | 
							account.Emojis = existing.Emojis
 | 
				
			||||||
	if maybeLen != gotLen {
 | 
							return nil
 | 
				
			||||||
		changed = true
 | 
					 | 
				
			||||||
		targetAccount.Emojis = gotEmojis
 | 
					 | 
				
			||||||
		targetAccount.EmojiIDs = gotEmojiIDs
 | 
					 | 
				
			||||||
		return changed, nil
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// if the lengths are the same but not all of the slices are
 | 
						// Set latest emojis.
 | 
				
			||||||
	// zero, something *might* have changed, so we have to check
 | 
						account.Emojis = emojis
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 1. did we have emojis before that we don't have now?
 | 
						// Iterate over and set changed emoji IDs.
 | 
				
			||||||
	for _, maybeEmoji := range maybeEmojis {
 | 
						account.EmojiIDs = make([]string, len(emojis))
 | 
				
			||||||
		var stillPresent bool
 | 
						for i, emoji := range emojis {
 | 
				
			||||||
 | 
							account.EmojiIDs[i] = emoji.ID
 | 
				
			||||||
		for _, gotEmoji := range gotEmojis {
 | 
					 | 
				
			||||||
			if maybeEmoji.URI == gotEmoji.URI {
 | 
					 | 
				
			||||||
				// the emoji we maybe had is still present now,
 | 
					 | 
				
			||||||
				// so we can stop checking gotEmojis
 | 
					 | 
				
			||||||
				stillPresent = true
 | 
					 | 
				
			||||||
				break
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if !stillPresent {
 | 
						return nil
 | 
				
			||||||
			// at least one maybeEmoji is no longer present in
 | 
					 | 
				
			||||||
			// the got emojis, so we can stop checking now
 | 
					 | 
				
			||||||
			changed = true
 | 
					 | 
				
			||||||
			targetAccount.Emojis = gotEmojis
 | 
					 | 
				
			||||||
			targetAccount.EmojiIDs = gotEmojiIDs
 | 
					 | 
				
			||||||
			return changed, nil
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 2. do we have emojis now that we didn't have before?
 | 
					func (d *Dereferencer) dereferenceAccountStats(
 | 
				
			||||||
	for _, gotEmoji := range gotEmojis {
 | 
						ctx context.Context,
 | 
				
			||||||
		var wasPresent bool
 | 
						requestUser string,
 | 
				
			||||||
 | 
						account *gtsmodel.Account,
 | 
				
			||||||
		for _, maybeEmoji := range maybeEmojis {
 | 
					) error {
 | 
				
			||||||
			// check emoji IDs here as well, because unreferenced
 | 
					 | 
				
			||||||
			// maybe emojis we didn't already have would not have
 | 
					 | 
				
			||||||
			// had IDs set on them yet
 | 
					 | 
				
			||||||
			if gotEmoji.URI == maybeEmoji.URI && gotEmoji.ID == maybeEmoji.ID {
 | 
					 | 
				
			||||||
				// this got emoji was present already in the maybeEmoji,
 | 
					 | 
				
			||||||
				// so we can stop checking through maybeEmojis
 | 
					 | 
				
			||||||
				wasPresent = true
 | 
					 | 
				
			||||||
				break
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if !wasPresent {
 | 
					 | 
				
			||||||
			// at least one gotEmojis was not present in
 | 
					 | 
				
			||||||
			// the maybeEmojis, so we can stop checking now
 | 
					 | 
				
			||||||
			changed = true
 | 
					 | 
				
			||||||
			targetAccount.Emojis = gotEmojis
 | 
					 | 
				
			||||||
			targetAccount.EmojiIDs = gotEmojiIDs
 | 
					 | 
				
			||||||
			return changed, nil
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return changed, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (d *Dereferencer) dereferenceAccountStats(ctx context.Context, requestUser string, account *gtsmodel.Account) error {
 | 
					 | 
				
			||||||
	// Ensure we have a stats model for this account.
 | 
						// Ensure we have a stats model for this account.
 | 
				
			||||||
	if account.Stats == nil {
 | 
						if account.Stats == nil {
 | 
				
			||||||
		if err := d.state.DB.PopulateAccountStats(ctx, account); err != nil {
 | 
							if err := d.state.DB.PopulateAccountStats(ctx, account); err != nil {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,29 +19,190 @@ package dereferencing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"fmt"
 | 
						"errors"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/db"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/db"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
 | 
						"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/log"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/log"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/media"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/media"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/util"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/util"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (d *Dereferencer) GetRemoteEmoji(ctx context.Context, requestUser string, remoteURL string, shortcode string, domain string, id string, emojiURI string, ai *media.AdditionalEmojiInfo, refresh bool) (*media.ProcessingEmoji, error) {
 | 
					// GetEmoji fetches the emoji with given shortcode,
 | 
				
			||||||
	var shortcodeDomain = shortcode + "@" + domain
 | 
					// domain and remote URL to dereference it by. This
 | 
				
			||||||
 | 
					// handles the case of existing emojis by passing them
 | 
				
			||||||
	// Ensure we have been passed a valid URL.
 | 
					// to RefreshEmoji(), which in the case of a local
 | 
				
			||||||
	derefURI, err := url.Parse(remoteURL)
 | 
					// emoji will be a no-op. If the emoji does not yet
 | 
				
			||||||
	if err != nil {
 | 
					// exist it will be newly inserted into the database
 | 
				
			||||||
		return nil, fmt.Errorf("GetRemoteEmoji: error parsing url for emoji %s: %s", shortcodeDomain, err)
 | 
					// followed by dereferencing the actual media file.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Please note that even if an error is returned,
 | 
				
			||||||
 | 
					// an emoji model may still be returned if the error
 | 
				
			||||||
 | 
					// was only encountered during actual dereferencing.
 | 
				
			||||||
 | 
					// In this case, it will act as a placeholder.
 | 
				
			||||||
 | 
					func (d *Dereferencer) GetEmoji(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						shortcode string,
 | 
				
			||||||
 | 
						domain string,
 | 
				
			||||||
 | 
						remoteURL string,
 | 
				
			||||||
 | 
						info media.AdditionalEmojiInfo,
 | 
				
			||||||
 | 
						refresh bool,
 | 
				
			||||||
 | 
					) (
 | 
				
			||||||
 | 
						*gtsmodel.Emoji,
 | 
				
			||||||
 | 
						error,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						// Look for an existing emoji with shortcode domain.
 | 
				
			||||||
 | 
						emoji, err := d.state.DB.GetEmojiByShortcodeDomain(ctx,
 | 
				
			||||||
 | 
							shortcode,
 | 
				
			||||||
 | 
							domain,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if err != nil && !errors.Is(err, db.ErrNoEntries) {
 | 
				
			||||||
 | 
							return nil, gtserror.Newf("error fetching emoji from db: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Acquire derefs lock.
 | 
						if emoji != nil {
 | 
				
			||||||
 | 
							// This was an existing emoji, pass to refresh func.
 | 
				
			||||||
 | 
							return d.RefreshEmoji(ctx, emoji, info, refresh)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if domain == "" {
 | 
				
			||||||
 | 
							// failed local lookup, will be db.ErrNoEntries.
 | 
				
			||||||
 | 
							return nil, gtserror.SetUnretrievable(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Generate shortcode domain for locks + logging.
 | 
				
			||||||
 | 
						shortcodeDomain := shortcode + "@" + domain
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ensure we have a valid remote URL.
 | 
				
			||||||
 | 
						url, err := url.Parse(remoteURL)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							err := gtserror.Newf("invalid image remote url %s for emoji %s: %w", remoteURL, shortcodeDomain, err)
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Acquire new instance account transport for emoji dereferencing.
 | 
				
			||||||
 | 
						tsport, err := d.transportController.NewTransportForUsername(ctx, "")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							err := gtserror.Newf("error getting instance transport: %w", err)
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Prepare data function to dereference remote emoji media.
 | 
				
			||||||
 | 
						data := func(context.Context) (io.ReadCloser, int64, error) {
 | 
				
			||||||
 | 
							return tsport.DereferenceMedia(ctx, url)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Pass along for safe processing.
 | 
				
			||||||
 | 
						return d.processEmojiSafely(ctx,
 | 
				
			||||||
 | 
							shortcodeDomain,
 | 
				
			||||||
 | 
							func() (*media.ProcessingEmoji, error) {
 | 
				
			||||||
 | 
								return d.mediaManager.CreateEmoji(ctx,
 | 
				
			||||||
 | 
									shortcode,
 | 
				
			||||||
 | 
									domain,
 | 
				
			||||||
 | 
									data,
 | 
				
			||||||
 | 
									info,
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RefreshEmoji ensures that the given emoji is
 | 
				
			||||||
 | 
					// up-to-date, both in terms of being cached in
 | 
				
			||||||
 | 
					// in local instance storage, and compared to extra
 | 
				
			||||||
 | 
					// information provided in media.AdditionEmojiInfo{}.
 | 
				
			||||||
 | 
					// (note that is a no-op to pass in a local emoji).
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Please note that even if an error is returned,
 | 
				
			||||||
 | 
					// an emoji model may still be returned if the error
 | 
				
			||||||
 | 
					// was only encountered during actual dereferencing.
 | 
				
			||||||
 | 
					// In this case, it will act as a placeholder.
 | 
				
			||||||
 | 
					func (d *Dereferencer) RefreshEmoji(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						emoji *gtsmodel.Emoji,
 | 
				
			||||||
 | 
						info media.AdditionalEmojiInfo,
 | 
				
			||||||
 | 
						force bool,
 | 
				
			||||||
 | 
					) (
 | 
				
			||||||
 | 
						*gtsmodel.Emoji,
 | 
				
			||||||
 | 
						error,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						// Can't refresh local.
 | 
				
			||||||
 | 
						if emoji.IsLocal() {
 | 
				
			||||||
 | 
							return emoji, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check emoji is up-to-date
 | 
				
			||||||
 | 
						// with provided extra info.
 | 
				
			||||||
 | 
						switch {
 | 
				
			||||||
 | 
						case info.URI != nil &&
 | 
				
			||||||
 | 
							*info.URI != emoji.URI:
 | 
				
			||||||
 | 
							force = true
 | 
				
			||||||
 | 
						case info.ImageRemoteURL != nil &&
 | 
				
			||||||
 | 
							*info.ImageRemoteURL != emoji.ImageRemoteURL:
 | 
				
			||||||
 | 
							force = true
 | 
				
			||||||
 | 
						case info.ImageStaticRemoteURL != nil &&
 | 
				
			||||||
 | 
							*info.ImageStaticRemoteURL != emoji.ImageStaticRemoteURL:
 | 
				
			||||||
 | 
							force = true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check if needs updating.
 | 
				
			||||||
 | 
						if !force && *emoji.Cached {
 | 
				
			||||||
 | 
							return emoji, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO: more finegrained freshness checks.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Generate shortcode domain for locks + logging.
 | 
				
			||||||
 | 
						shortcodeDomain := emoji.Shortcode + "@" + emoji.Domain
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ensure we have a valid image remote URL.
 | 
				
			||||||
 | 
						url, err := url.Parse(emoji.ImageRemoteURL)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							err := gtserror.Newf("invalid image remote url %s for emoji %s: %w", emoji.ImageRemoteURL, shortcodeDomain, err)
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Acquire new instance account transport for emoji dereferencing.
 | 
				
			||||||
 | 
						tsport, err := d.transportController.NewTransportForUsername(ctx, "")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							err := gtserror.Newf("error getting instance transport: %w", err)
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Prepare data function to dereference remote emoji media.
 | 
				
			||||||
 | 
						data := func(context.Context) (io.ReadCloser, int64, error) {
 | 
				
			||||||
 | 
							return tsport.DereferenceMedia(ctx, url)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Pass along for safe processing.
 | 
				
			||||||
 | 
						return d.processEmojiSafely(ctx,
 | 
				
			||||||
 | 
							shortcodeDomain,
 | 
				
			||||||
 | 
							func() (*media.ProcessingEmoji, error) {
 | 
				
			||||||
 | 
								return d.mediaManager.RefreshEmoji(ctx,
 | 
				
			||||||
 | 
									emoji,
 | 
				
			||||||
 | 
									data,
 | 
				
			||||||
 | 
									info,
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// processingEmojiSafely provides concurrency-safe processing of
 | 
				
			||||||
 | 
					// an emoji with given shortcode+domain. if a copy of the emoji is
 | 
				
			||||||
 | 
					// not already being processed, the given 'process' callback will
 | 
				
			||||||
 | 
					// be used to generate new *media.ProcessingEmoji{} instance.
 | 
				
			||||||
 | 
					func (d *Dereferencer) processEmojiSafely(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						shortcodeDomain string,
 | 
				
			||||||
 | 
						process func() (*media.ProcessingEmoji, error),
 | 
				
			||||||
 | 
					) (
 | 
				
			||||||
 | 
						emoji *gtsmodel.Emoji,
 | 
				
			||||||
 | 
						err error,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Acquire map lock.
 | 
				
			||||||
	d.derefEmojisMu.Lock()
 | 
						d.derefEmojisMu.Lock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Ensure unlock only done once.
 | 
						// Ensure unlock only done once.
 | 
				
			||||||
| 
						 | 
					@ -53,146 +214,118 @@ func (d *Dereferencer) GetRemoteEmoji(ctx context.Context, requestUser string, r
 | 
				
			||||||
	processing, ok := d.derefEmojis[shortcodeDomain]
 | 
						processing, ok := d.derefEmojis[shortcodeDomain]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if !ok {
 | 
						if !ok {
 | 
				
			||||||
		// Fetch a transport for current request user in order to perform request.
 | 
							// Start new processing emoji.
 | 
				
			||||||
		tsport, err := d.transportController.NewTransportForUsername(ctx, requestUser)
 | 
							processing, err = process()
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return nil, gtserror.Newf("couldn't create transport: %w", err)
 | 
								return nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Set the media data function to dereference emoji from URI.
 | 
					 | 
				
			||||||
		data := func(ctx context.Context) (io.ReadCloser, int64, error) {
 | 
					 | 
				
			||||||
			return tsport.DereferenceMedia(ctx, derefURI)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Create new emoji processing request from the media manager.
 | 
					 | 
				
			||||||
		processing, err = d.mediaManager.PreProcessEmoji(ctx, data,
 | 
					 | 
				
			||||||
			shortcode,
 | 
					 | 
				
			||||||
			id,
 | 
					 | 
				
			||||||
			emojiURI,
 | 
					 | 
				
			||||||
			ai,
 | 
					 | 
				
			||||||
			refresh,
 | 
					 | 
				
			||||||
		)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return nil, gtserror.Newf("error preprocessing emoji %s: %s", shortcodeDomain, err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Store media in map to mark as processing.
 | 
					 | 
				
			||||||
		d.derefEmojis[shortcodeDomain] = processing
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		defer func() {
 | 
					 | 
				
			||||||
			// On exit safely remove emoji from map.
 | 
					 | 
				
			||||||
			d.derefEmojisMu.Lock()
 | 
					 | 
				
			||||||
			delete(d.derefEmojis, shortcodeDomain)
 | 
					 | 
				
			||||||
			d.derefEmojisMu.Unlock()
 | 
					 | 
				
			||||||
		}()
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Unlock map.
 | 
						// Unlock map.
 | 
				
			||||||
	unlock()
 | 
						unlock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Start emoji attachment loading (blocking call).
 | 
						// Perform emoji load operation.
 | 
				
			||||||
	if _, err := processing.LoadEmoji(ctx); err != nil {
 | 
						emoji, err = processing.Load(ctx)
 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return processing, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (d *Dereferencer) populateEmojis(ctx context.Context, rawEmojis []*gtsmodel.Emoji, requestingUsername string) ([]*gtsmodel.Emoji, error) {
 | 
					 | 
				
			||||||
	// At this point we should know:
 | 
					 | 
				
			||||||
	// * the AP uri of the emoji
 | 
					 | 
				
			||||||
	// * the domain of the emoji
 | 
					 | 
				
			||||||
	// * the shortcode of the emoji
 | 
					 | 
				
			||||||
	// * the remote URL of the image
 | 
					 | 
				
			||||||
	// This should be enough to dereference the emoji
 | 
					 | 
				
			||||||
	gotEmojis := make([]*gtsmodel.Emoji, 0, len(rawEmojis))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	for _, e := range rawEmojis {
 | 
					 | 
				
			||||||
		var gotEmoji *gtsmodel.Emoji
 | 
					 | 
				
			||||||
		var err error
 | 
					 | 
				
			||||||
		shortcodeDomain := e.Shortcode + "@" + e.Domain
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// check if we already know this emoji
 | 
					 | 
				
			||||||
		if e.ID != "" {
 | 
					 | 
				
			||||||
			// we had an ID for this emoji already, which means
 | 
					 | 
				
			||||||
			// it should be fleshed out already and we won't
 | 
					 | 
				
			||||||
			// have to get it from the database again
 | 
					 | 
				
			||||||
			gotEmoji = e
 | 
					 | 
				
			||||||
		} else if gotEmoji, err = d.state.DB.GetEmojiByShortcodeDomain(ctx, e.Shortcode, e.Domain); err != nil && err != db.ErrNoEntries {
 | 
					 | 
				
			||||||
			log.Errorf(ctx, "error checking database for emoji %s: %s", shortcodeDomain, err)
 | 
					 | 
				
			||||||
			continue
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		var refresh bool
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if gotEmoji != nil {
 | 
					 | 
				
			||||||
			// we had the emoji already, but refresh it if necessary
 | 
					 | 
				
			||||||
			if e.UpdatedAt.Unix() > gotEmoji.ImageUpdatedAt.Unix() {
 | 
					 | 
				
			||||||
				log.Tracef(ctx, "emoji %s was updated since we last saw it, will refresh", shortcodeDomain)
 | 
					 | 
				
			||||||
				refresh = true
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if !refresh && (e.URI != gotEmoji.URI) {
 | 
					 | 
				
			||||||
				log.Tracef(ctx, "emoji %s changed URI since we last saw it, will refresh", shortcodeDomain)
 | 
					 | 
				
			||||||
				refresh = true
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if !refresh && (e.ImageRemoteURL != gotEmoji.ImageRemoteURL) {
 | 
					 | 
				
			||||||
				log.Tracef(ctx, "emoji %s changed image URL since we last saw it, will refresh", shortcodeDomain)
 | 
					 | 
				
			||||||
				refresh = true
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if !refresh {
 | 
					 | 
				
			||||||
				log.Tracef(ctx, "emoji %s is up to date, will not refresh", shortcodeDomain)
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				log.Tracef(ctx, "refreshing emoji %s", shortcodeDomain)
 | 
					 | 
				
			||||||
				emojiID := gotEmoji.ID // use existing ID
 | 
					 | 
				
			||||||
				processingEmoji, err := d.GetRemoteEmoji(ctx, requestingUsername, e.ImageRemoteURL, e.Shortcode, e.Domain, emojiID, e.URI, &media.AdditionalEmojiInfo{
 | 
					 | 
				
			||||||
					Domain:               &e.Domain,
 | 
					 | 
				
			||||||
					ImageRemoteURL:       &e.ImageRemoteURL,
 | 
					 | 
				
			||||||
					ImageStaticRemoteURL: &e.ImageStaticRemoteURL,
 | 
					 | 
				
			||||||
					Disabled:             gotEmoji.Disabled,
 | 
					 | 
				
			||||||
					VisibleInPicker:      gotEmoji.VisibleInPicker,
 | 
					 | 
				
			||||||
				}, refresh)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
					log.Errorf(ctx, "couldn't refresh remote emoji %s: %s", shortcodeDomain, err)
 | 
							err = gtserror.Newf("error loading emoji %s: %w", shortcodeDomain, err)
 | 
				
			||||||
					continue
 | 
					
 | 
				
			||||||
 | 
							// TODO: in time we should return checkable flags by gtserror.Is___()
 | 
				
			||||||
 | 
							// which can determine if loading error should allow remaining placeholder.
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if gotEmoji, err = processingEmoji.LoadEmoji(ctx); err != nil {
 | 
						// Return a COPY of emoji.
 | 
				
			||||||
					log.Errorf(ctx, "couldn't load refreshed remote emoji %s: %s", shortcodeDomain, err)
 | 
						emoji2 := new(gtsmodel.Emoji)
 | 
				
			||||||
					continue
 | 
						*emoji2 = *emoji
 | 
				
			||||||
 | 
						return emoji2, err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
			}
 | 
					
 | 
				
			||||||
		} else {
 | 
					func (d *Dereferencer) fetchEmojis(
 | 
				
			||||||
			// it's new! go get it!
 | 
						ctx context.Context,
 | 
				
			||||||
			newEmojiID, err := id.NewRandomULID()
 | 
						existing []*gtsmodel.Emoji,
 | 
				
			||||||
 | 
						emojis []*gtsmodel.Emoji, // newly dereferenced
 | 
				
			||||||
 | 
					) (
 | 
				
			||||||
 | 
						[]*gtsmodel.Emoji,
 | 
				
			||||||
 | 
						bool, // any changes?
 | 
				
			||||||
 | 
						error,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						// Track any changes.
 | 
				
			||||||
 | 
						changed := false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for i, placeholder := range emojis {
 | 
				
			||||||
 | 
							// Look for an existing emoji with shortcode + domain.
 | 
				
			||||||
 | 
							existing, ok := getEmojiByShortcodeDomain(existing,
 | 
				
			||||||
 | 
								placeholder.Shortcode,
 | 
				
			||||||
 | 
								placeholder.Domain,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							if ok && existing.ID != "" {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Check for any emoji changes that
 | 
				
			||||||
 | 
								// indicate we should force a refresh.
 | 
				
			||||||
 | 
								force := emojiChanged(existing, placeholder)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Ensure that the existing emoji model is up-to-date and cached.
 | 
				
			||||||
 | 
								existing, err := d.RefreshEmoji(ctx, existing, media.AdditionalEmojiInfo{
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Set latest values from placeholder.
 | 
				
			||||||
 | 
									URI:                  &placeholder.URI,
 | 
				
			||||||
 | 
									ImageRemoteURL:       &placeholder.ImageRemoteURL,
 | 
				
			||||||
 | 
									ImageStaticRemoteURL: &placeholder.ImageStaticRemoteURL,
 | 
				
			||||||
 | 
								}, force)
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				log.Errorf(ctx, "error generating id for remote emoji %s: %s", shortcodeDomain, err)
 | 
									log.Errorf(ctx, "error refreshing emoji: %v", err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// specifically do NOT continue here,
 | 
				
			||||||
 | 
									// we already have a model, we don't
 | 
				
			||||||
 | 
									// want to drop it from the slice, just
 | 
				
			||||||
 | 
									// log that an update for it failed.
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Set existing emoji.
 | 
				
			||||||
 | 
								emojis[i] = existing
 | 
				
			||||||
			continue
 | 
								continue
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			processingEmoji, err := d.GetRemoteEmoji(ctx, requestingUsername, e.ImageRemoteURL, e.Shortcode, e.Domain, newEmojiID, e.URI, &media.AdditionalEmojiInfo{
 | 
							// Emojis changed!
 | 
				
			||||||
				Domain:               &e.Domain,
 | 
							changed = true
 | 
				
			||||||
				ImageRemoteURL:       &e.ImageRemoteURL,
 | 
					
 | 
				
			||||||
				ImageStaticRemoteURL: &e.ImageStaticRemoteURL,
 | 
							// Fetch this newly added emoji,
 | 
				
			||||||
				Disabled:             e.Disabled,
 | 
							// this function handles the case
 | 
				
			||||||
				VisibleInPicker:      e.VisibleInPicker,
 | 
							// of existing cached emojis and
 | 
				
			||||||
			}, refresh)
 | 
							// new ones requiring dereference.
 | 
				
			||||||
 | 
							emoji, err := d.GetEmoji(ctx,
 | 
				
			||||||
 | 
								placeholder.Shortcode,
 | 
				
			||||||
 | 
								placeholder.Domain,
 | 
				
			||||||
 | 
								placeholder.ImageRemoteURL,
 | 
				
			||||||
 | 
								media.AdditionalEmojiInfo{
 | 
				
			||||||
 | 
									URI:                  &placeholder.URI,
 | 
				
			||||||
 | 
									ImageRemoteURL:       &placeholder.ImageRemoteURL,
 | 
				
			||||||
 | 
									ImageStaticRemoteURL: &placeholder.ImageStaticRemoteURL,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								false,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
				log.Errorf(ctx, "couldn't get remote emoji %s: %s", shortcodeDomain, err)
 | 
								if emoji == nil {
 | 
				
			||||||
 | 
									log.Errorf(ctx, "error loading emoji %s: %v", placeholder.ImageRemoteURL, err)
 | 
				
			||||||
				continue
 | 
									continue
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if gotEmoji, err = processingEmoji.LoadEmoji(ctx); err != nil {
 | 
								// non-fatal error occurred during loading, still use it.
 | 
				
			||||||
				log.Errorf(ctx, "couldn't load remote emoji %s: %s", shortcodeDomain, err)
 | 
								log.Warnf(ctx, "partially loaded emoji: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Set updated emoji.
 | 
				
			||||||
 | 
							emojis[i] = emoji
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for i := 0; i < len(emojis); {
 | 
				
			||||||
 | 
							if emojis[i].ID == "" {
 | 
				
			||||||
 | 
								// Remove failed emoji populations.
 | 
				
			||||||
 | 
								copy(emojis[i:], emojis[i+1:])
 | 
				
			||||||
 | 
								emojis = emojis[:len(emojis)-1]
 | 
				
			||||||
			continue
 | 
								continue
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							i++
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// if we get here, we either had the emoji already or we successfully fetched it
 | 
						return emojis, changed, nil
 | 
				
			||||||
		gotEmojis = append(gotEmojis, gotEmoji)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return gotEmojis, nil
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,6 +19,7 @@ package dereferencing_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -32,48 +33,50 @@ type EmojiTestSuite struct {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (suite *EmojiTestSuite) TestDereferenceEmojiBlocking() {
 | 
					func (suite *EmojiTestSuite) TestDereferenceEmojiBlocking() {
 | 
				
			||||||
	ctx := context.Background()
 | 
						ctx := context.Background()
 | 
				
			||||||
	fetchingAccount := suite.testAccounts["local_account_1"]
 | 
					 | 
				
			||||||
	emojiImageRemoteURL := "http://example.org/media/emojis/1781772.gif"
 | 
						emojiImageRemoteURL := "http://example.org/media/emojis/1781772.gif"
 | 
				
			||||||
	emojiImageStaticRemoteURL := "http://example.org/media/emojis/1781772.gif"
 | 
						emojiImageStaticRemoteURL := "http://example.org/media/emojis/1781772.gif"
 | 
				
			||||||
	emojiURI := "http://example.org/emojis/1781772"
 | 
						emojiURI := "http://example.org/emojis/1781772"
 | 
				
			||||||
	emojiShortcode := "peglin"
 | 
						emojiShortcode := "peglin"
 | 
				
			||||||
	emojiID := "01GCBMGNZBKMEE1KTZ6PMJEW5D"
 | 
					 | 
				
			||||||
	emojiDomain := "example.org"
 | 
						emojiDomain := "example.org"
 | 
				
			||||||
	emojiDisabled := false
 | 
						emojiDisabled := false
 | 
				
			||||||
	emojiVisibleInPicker := false
 | 
						emojiVisibleInPicker := false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ai := &media.AdditionalEmojiInfo{
 | 
						emoji, err := suite.dereferencer.GetEmoji(
 | 
				
			||||||
 | 
							ctx,
 | 
				
			||||||
 | 
							emojiShortcode,
 | 
				
			||||||
 | 
							emojiDomain,
 | 
				
			||||||
 | 
							emojiImageRemoteURL,
 | 
				
			||||||
 | 
							media.AdditionalEmojiInfo{
 | 
				
			||||||
 | 
								URI:                  &emojiURI,
 | 
				
			||||||
			Domain:               &emojiDomain,
 | 
								Domain:               &emojiDomain,
 | 
				
			||||||
			ImageRemoteURL:       &emojiImageRemoteURL,
 | 
								ImageRemoteURL:       &emojiImageRemoteURL,
 | 
				
			||||||
			ImageStaticRemoteURL: &emojiImageStaticRemoteURL,
 | 
								ImageStaticRemoteURL: &emojiImageStaticRemoteURL,
 | 
				
			||||||
			Disabled:             &emojiDisabled,
 | 
								Disabled:             &emojiDisabled,
 | 
				
			||||||
			VisibleInPicker:      &emojiVisibleInPicker,
 | 
								VisibleInPicker:      &emojiVisibleInPicker,
 | 
				
			||||||
	}
 | 
							},
 | 
				
			||||||
 | 
							false,
 | 
				
			||||||
	processingEmoji, err := suite.dereferencer.GetRemoteEmoji(ctx, fetchingAccount.Username, emojiImageRemoteURL, emojiShortcode, emojiDomain, emojiID, emojiURI, ai, false)
 | 
						)
 | 
				
			||||||
	suite.NoError(err)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// make a blocking call to load the emoji from the in-process media
 | 
					 | 
				
			||||||
	emoji, err := processingEmoji.LoadEmoji(ctx)
 | 
					 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(emoji)
 | 
						suite.NotNil(emoji)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	suite.Equal(emojiID, emoji.ID)
 | 
						expectPath := fmt.Sprintf("/emoji/original/%s.gif", emoji.ID)
 | 
				
			||||||
 | 
						expectStaticPath := fmt.Sprintf("/emoji/static/%s.png", emoji.ID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	suite.WithinDuration(time.Now(), emoji.CreatedAt, 10*time.Second)
 | 
						suite.WithinDuration(time.Now(), emoji.CreatedAt, 10*time.Second)
 | 
				
			||||||
	suite.WithinDuration(time.Now(), emoji.UpdatedAt, 10*time.Second)
 | 
						suite.WithinDuration(time.Now(), emoji.UpdatedAt, 10*time.Second)
 | 
				
			||||||
	suite.Equal(emojiShortcode, emoji.Shortcode)
 | 
						suite.Equal(emojiShortcode, emoji.Shortcode)
 | 
				
			||||||
	suite.Equal(emojiDomain, emoji.Domain)
 | 
						suite.Equal(emojiDomain, emoji.Domain)
 | 
				
			||||||
	suite.Equal(emojiImageRemoteURL, emoji.ImageRemoteURL)
 | 
						suite.Equal(emojiImageRemoteURL, emoji.ImageRemoteURL)
 | 
				
			||||||
	suite.Equal(emojiImageStaticRemoteURL, emoji.ImageStaticRemoteURL)
 | 
						suite.Equal(emojiImageStaticRemoteURL, emoji.ImageStaticRemoteURL)
 | 
				
			||||||
	suite.Contains(emoji.ImageURL, "/emoji/original/01GCBMGNZBKMEE1KTZ6PMJEW5D.gif")
 | 
						suite.Contains(emoji.ImageURL, expectPath)
 | 
				
			||||||
	suite.Contains(emoji.ImageStaticURL, "emoji/static/01GCBMGNZBKMEE1KTZ6PMJEW5D.png")
 | 
						suite.Contains(emoji.ImageStaticURL, expectStaticPath)
 | 
				
			||||||
	suite.Contains(emoji.ImagePath, "/emoji/original/01GCBMGNZBKMEE1KTZ6PMJEW5D.gif")
 | 
						suite.Contains(emoji.ImagePath, expectPath)
 | 
				
			||||||
	suite.Contains(emoji.ImageStaticPath, "/emoji/static/01GCBMGNZBKMEE1KTZ6PMJEW5D.png")
 | 
						suite.Contains(emoji.ImageStaticPath, expectStaticPath)
 | 
				
			||||||
	suite.Equal("image/gif", emoji.ImageContentType)
 | 
						suite.Equal("image/gif", emoji.ImageContentType)
 | 
				
			||||||
	suite.Equal("image/png", emoji.ImageStaticContentType)
 | 
						suite.Equal("image/png", emoji.ImageStaticContentType)
 | 
				
			||||||
	suite.Equal(37796, emoji.ImageFileSize)
 | 
						suite.Equal(37796, emoji.ImageFileSize)
 | 
				
			||||||
	suite.Equal(7951, emoji.ImageStaticFileSize)
 | 
						suite.Equal(7951, emoji.ImageStaticFileSize)
 | 
				
			||||||
	suite.WithinDuration(time.Now(), emoji.ImageUpdatedAt, 10*time.Second)
 | 
						suite.WithinDuration(time.Now(), emoji.UpdatedAt, 10*time.Second)
 | 
				
			||||||
	suite.False(*emoji.Disabled)
 | 
						suite.False(*emoji.Disabled)
 | 
				
			||||||
	suite.Equal(emojiURI, emoji.URI)
 | 
						suite.Equal(emojiURI, emoji.URI)
 | 
				
			||||||
	suite.False(*emoji.VisibleInPicker)
 | 
						suite.False(*emoji.VisibleInPicker)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										215
									
								
								internal/federation/dereferencing/media.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								internal/federation/dereferencing/media.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,215 @@
 | 
				
			||||||
 | 
					// GoToSocial
 | 
				
			||||||
 | 
					// Copyright (C) GoToSocial Authors admin@gotosocial.org
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: AGPL-3.0-or-later
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// 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 dereferencing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/superseriousbusiness/gotosocial/internal/gtserror"
 | 
				
			||||||
 | 
						"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 | 
				
			||||||
 | 
						"github.com/superseriousbusiness/gotosocial/internal/media"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetMedia fetches the media at given remote URL by
 | 
				
			||||||
 | 
					// dereferencing it. The passed accountID is used to
 | 
				
			||||||
 | 
					// store it as being owned by that account. Additional
 | 
				
			||||||
 | 
					// information to set on the media attachment may also
 | 
				
			||||||
 | 
					// be provided.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Please note that even if an error is returned,
 | 
				
			||||||
 | 
					// a media model may still be returned if the error
 | 
				
			||||||
 | 
					// was only encountered during actual dereferencing.
 | 
				
			||||||
 | 
					// In this case, it will act as a placeholder.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Also note that since account / status dereferencing is
 | 
				
			||||||
 | 
					// already protected by per-uri locks, and that fediverse
 | 
				
			||||||
 | 
					// media is generally not shared between accounts (etc),
 | 
				
			||||||
 | 
					// there aren't any concurrency protections against multiple
 | 
				
			||||||
 | 
					// insertion / dereferencing of media at remoteURL. Worst
 | 
				
			||||||
 | 
					// case scenario, an extra media entry will be inserted
 | 
				
			||||||
 | 
					// and the scheduled cleaner.Cleaner{} will catch it!
 | 
				
			||||||
 | 
					func (d *Dereferencer) GetMedia(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						requestUser string,
 | 
				
			||||||
 | 
						accountID string, // media account owner
 | 
				
			||||||
 | 
						remoteURL string,
 | 
				
			||||||
 | 
						info media.AdditionalMediaInfo,
 | 
				
			||||||
 | 
					) (
 | 
				
			||||||
 | 
						*gtsmodel.MediaAttachment,
 | 
				
			||||||
 | 
						error,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						// Parse str as valid URL object.
 | 
				
			||||||
 | 
						url, err := url.Parse(remoteURL)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, gtserror.Newf("invalid remote media url %q: %v", remoteURL, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Fetch transport for the provided request user from controller.
 | 
				
			||||||
 | 
						tsport, err := d.transportController.NewTransportForUsername(ctx,
 | 
				
			||||||
 | 
							requestUser,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, gtserror.Newf("failed getting transport for %s: %w", requestUser, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Start processing remote attachment at URL.
 | 
				
			||||||
 | 
						processing, err := d.mediaManager.CreateMedia(
 | 
				
			||||||
 | 
							ctx,
 | 
				
			||||||
 | 
							accountID,
 | 
				
			||||||
 | 
							func(ctx context.Context) (io.ReadCloser, int64, error) {
 | 
				
			||||||
 | 
								return tsport.DereferenceMedia(ctx, url)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							info,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Perform media load operation.
 | 
				
			||||||
 | 
						media, err := processing.Load(ctx)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							err = gtserror.Newf("error loading media %s: %w", media.RemoteURL, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// TODO: in time we should return checkable flags by gtserror.Is___()
 | 
				
			||||||
 | 
							// which can determine if loading error should allow remaining placeholder.
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return media, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RefreshMedia ensures that given media is up-to-date,
 | 
				
			||||||
 | 
					// both in terms of being cached in local instance,
 | 
				
			||||||
 | 
					// storage and compared to extra info in information
 | 
				
			||||||
 | 
					// in given gtsmodel.AdditionMediaInfo{}. This handles
 | 
				
			||||||
 | 
					// the case of local emoji by returning early.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Please note that even if an error is returned,
 | 
				
			||||||
 | 
					// a media model may still be returned if the error
 | 
				
			||||||
 | 
					// was only encountered during actual dereferencing.
 | 
				
			||||||
 | 
					// In this case, it will act as a placeholder.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Also note that since account / status dereferencing is
 | 
				
			||||||
 | 
					// already protected by per-uri locks, and that fediverse
 | 
				
			||||||
 | 
					// media is generally not shared between accounts (etc),
 | 
				
			||||||
 | 
					// there aren't any concurrency protections against multiple
 | 
				
			||||||
 | 
					// insertion / dereferencing of media at remoteURL. Worst
 | 
				
			||||||
 | 
					// case scenario, an extra media entry will be inserted
 | 
				
			||||||
 | 
					// and the scheduled cleaner.Cleaner{} will catch it!
 | 
				
			||||||
 | 
					func (d *Dereferencer) RefreshMedia(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						requestUser string,
 | 
				
			||||||
 | 
						media *gtsmodel.MediaAttachment,
 | 
				
			||||||
 | 
						info media.AdditionalMediaInfo,
 | 
				
			||||||
 | 
						force bool,
 | 
				
			||||||
 | 
					) (
 | 
				
			||||||
 | 
						*gtsmodel.MediaAttachment,
 | 
				
			||||||
 | 
						error,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						// Can't refresh local.
 | 
				
			||||||
 | 
						if media.IsLocal() {
 | 
				
			||||||
 | 
							return media, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check emoji is up-to-date
 | 
				
			||||||
 | 
						// with provided extra info.
 | 
				
			||||||
 | 
						switch {
 | 
				
			||||||
 | 
						case info.Blurhash != nil &&
 | 
				
			||||||
 | 
							*info.Blurhash != media.Blurhash:
 | 
				
			||||||
 | 
							force = true
 | 
				
			||||||
 | 
						case info.Description != nil &&
 | 
				
			||||||
 | 
							*info.Description != media.Description:
 | 
				
			||||||
 | 
							force = true
 | 
				
			||||||
 | 
						case info.RemoteURL != nil &&
 | 
				
			||||||
 | 
							*info.RemoteURL != media.RemoteURL:
 | 
				
			||||||
 | 
							force = true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check if needs updating.
 | 
				
			||||||
 | 
						if !force && *media.Cached {
 | 
				
			||||||
 | 
							return media, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO: more finegrained freshness checks.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ensure we have a valid remote URL.
 | 
				
			||||||
 | 
						url, err := url.Parse(media.RemoteURL)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							err := gtserror.Newf("invalid media remote url %s: %w", media.RemoteURL, err)
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Fetch transport for the provided request user from controller.
 | 
				
			||||||
 | 
						tsport, err := d.transportController.NewTransportForUsername(ctx,
 | 
				
			||||||
 | 
							requestUser,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, gtserror.Newf("failed getting transport for %s: %w", requestUser, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Start processing remote attachment recache.
 | 
				
			||||||
 | 
						processing := d.mediaManager.RecacheMedia(
 | 
				
			||||||
 | 
							media,
 | 
				
			||||||
 | 
							func(ctx context.Context) (io.ReadCloser, int64, error) {
 | 
				
			||||||
 | 
								return tsport.DereferenceMedia(ctx, url)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Perform media load operation.
 | 
				
			||||||
 | 
						media, err = processing.Load(ctx)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							err = gtserror.Newf("error loading media %s: %w", media.RemoteURL, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// TODO: in time we should return checkable flags by gtserror.Is___()
 | 
				
			||||||
 | 
							// which can determine if loading error should allow remaining placeholder.
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return media, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// updateAttachment handles the case of an existing media attachment
 | 
				
			||||||
 | 
					// that *may* have changes or need recaching. it checks for changed
 | 
				
			||||||
 | 
					// fields, updating in the database if so, and recaches uncached media.
 | 
				
			||||||
 | 
					func (d *Dereferencer) updateAttachment(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						requestUser string,
 | 
				
			||||||
 | 
						existing *gtsmodel.MediaAttachment, // existing attachment
 | 
				
			||||||
 | 
						attach *gtsmodel.MediaAttachment, // (optional) changed media
 | 
				
			||||||
 | 
					) (
 | 
				
			||||||
 | 
						*gtsmodel.MediaAttachment, // always set
 | 
				
			||||||
 | 
						error,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						var info media.AdditionalMediaInfo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if attach != nil {
 | 
				
			||||||
 | 
							// Set optional extra information,
 | 
				
			||||||
 | 
							// (will later check for changes).
 | 
				
			||||||
 | 
							info.Description = &attach.Description
 | 
				
			||||||
 | 
							info.Blurhash = &attach.Blurhash
 | 
				
			||||||
 | 
							info.RemoteURL = &attach.RemoteURL
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ensure media is cached.
 | 
				
			||||||
 | 
						return d.RefreshMedia(ctx,
 | 
				
			||||||
 | 
							requestUser,
 | 
				
			||||||
 | 
							existing,
 | 
				
			||||||
 | 
							info,
 | 
				
			||||||
 | 
							false,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -33,7 +33,6 @@ import (
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/id"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/id"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/log"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/log"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/media"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/media"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/transport"
 | 
					 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/util"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/util"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -536,12 +535,12 @@ func (d *Dereferencer) enrichStatus(
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Ensure the status' media attachments are populated, passing in existing to check for changes.
 | 
						// Ensure the status' media attachments are populated, passing in existing to check for changes.
 | 
				
			||||||
	if err := d.fetchStatusAttachments(ctx, tsport, status, latestStatus); err != nil {
 | 
						if err := d.fetchStatusAttachments(ctx, requestUser, status, latestStatus); err != nil {
 | 
				
			||||||
		return nil, nil, gtserror.Newf("error populating attachments for status %s: %w", uri, err)
 | 
							return nil, nil, gtserror.Newf("error populating attachments for status %s: %w", uri, err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Ensure the status' emoji attachments are populated, (changes are expected / okay).
 | 
						// Ensure the status' emoji attachments are populated, passing in existing to check for changes.
 | 
				
			||||||
	if err := d.fetchStatusEmojis(ctx, requestUser, latestStatus); err != nil {
 | 
						if err := d.fetchStatusEmojis(ctx, status, latestStatus); err != nil {
 | 
				
			||||||
		return nil, nil, gtserror.Newf("error populating emojis for status %s: %w", uri, err)
 | 
							return nil, nil, gtserror.Newf("error populating emojis for status %s: %w", uri, err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -643,79 +642,12 @@ func (d *Dereferencer) isPermittedStatus(
 | 
				
			||||||
	return onFail()
 | 
						return onFail()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// populateMentionTarget tries to populate the given
 | 
					func (d *Dereferencer) fetchStatusMentions(
 | 
				
			||||||
// mention with the correct TargetAccount and (if not
 | 
					 | 
				
			||||||
// yet set) TargetAccountURI, returning the populated
 | 
					 | 
				
			||||||
// mention.
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// Will check on the existing status if the mention
 | 
					 | 
				
			||||||
// is already there and populated; if so, existing
 | 
					 | 
				
			||||||
// mention will be returned along with `true`.
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// Otherwise, this function will try to parse first
 | 
					 | 
				
			||||||
// the Href of the mention, and then the namestring,
 | 
					 | 
				
			||||||
// to see who it targets, and go fetch that account.
 | 
					 | 
				
			||||||
func (d *Dereferencer) populateMentionTarget(
 | 
					 | 
				
			||||||
	ctx context.Context,
 | 
						ctx context.Context,
 | 
				
			||||||
	mention *gtsmodel.Mention,
 | 
					 | 
				
			||||||
	requestUser string,
 | 
						requestUser string,
 | 
				
			||||||
	existing, status *gtsmodel.Status,
 | 
						existing *gtsmodel.Status,
 | 
				
			||||||
) (
 | 
						status *gtsmodel.Status,
 | 
				
			||||||
	*gtsmodel.Mention,
 | 
					) error {
 | 
				
			||||||
	bool, // True if mention already exists in the DB.
 | 
					 | 
				
			||||||
	error,
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
	// Mentions can be created using Name or Href.
 | 
					 | 
				
			||||||
	// Prefer Href (TargetAccountURI), fall back to Name.
 | 
					 | 
				
			||||||
	if mention.TargetAccountURI != "" {
 | 
					 | 
				
			||||||
		// Look for existing mention with this URI.
 | 
					 | 
				
			||||||
		// If we already have it we can return early.
 | 
					 | 
				
			||||||
		existingMention, ok := existing.GetMentionByTargetURI(mention.TargetAccountURI)
 | 
					 | 
				
			||||||
		if ok && existingMention.ID != "" {
 | 
					 | 
				
			||||||
			return existingMention, true, nil
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Ensure that mention account URI is parseable.
 | 
					 | 
				
			||||||
		accountURI, err := url.Parse(mention.TargetAccountURI)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			err = gtserror.Newf("invalid account uri %q: %w", mention.TargetAccountURI, err)
 | 
					 | 
				
			||||||
			return nil, false, err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Ensure we have the account of the mention target dereferenced.
 | 
					 | 
				
			||||||
		mention.TargetAccount, _, err = d.getAccountByURI(ctx, requestUser, accountURI)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			err = gtserror.Newf("failed to dereference account %s: %w", accountURI, err)
 | 
					 | 
				
			||||||
			return nil, false, err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		// Href wasn't set. Find the target account using namestring.
 | 
					 | 
				
			||||||
		username, domain, err := util.ExtractNamestringParts(mention.NameString)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			err = gtserror.Newf("failed to parse namestring %s: %w", mention.NameString, err)
 | 
					 | 
				
			||||||
			return nil, false, err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		mention.TargetAccount, _, err = d.getAccountByUsernameDomain(ctx, requestUser, username, domain)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			err = gtserror.Newf("failed to dereference account %s: %w", mention.NameString, err)
 | 
					 | 
				
			||||||
			return nil, false, err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Look for existing mention with this URI.
 | 
					 | 
				
			||||||
		mention.TargetAccountURI = mention.TargetAccount.URI
 | 
					 | 
				
			||||||
		existingMention, ok := existing.GetMentionByTargetURI(mention.TargetAccountURI)
 | 
					 | 
				
			||||||
		if ok && existingMention.ID != "" {
 | 
					 | 
				
			||||||
			return existingMention, true, nil
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// At this point, mention.TargetAccountURI
 | 
					 | 
				
			||||||
	// and mention.TargetAccount must be set.
 | 
					 | 
				
			||||||
	return mention, false, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (d *Dereferencer) fetchStatusMentions(ctx context.Context, requestUser string, existing, status *gtsmodel.Status) error {
 | 
					 | 
				
			||||||
	// Allocate new slice to take the yet-to-be created mention IDs.
 | 
						// Allocate new slice to take the yet-to-be created mention IDs.
 | 
				
			||||||
	status.MentionIDs = make([]string, len(status.Mentions))
 | 
						status.MentionIDs = make([]string, len(status.Mentions))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -728,10 +660,10 @@ func (d *Dereferencer) fetchStatusMentions(ctx context.Context, requestUser stri
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		mention, alreadyExists, err = d.populateMentionTarget(
 | 
							mention, alreadyExists, err = d.populateMentionTarget(
 | 
				
			||||||
			ctx,
 | 
								ctx,
 | 
				
			||||||
			mention,
 | 
					 | 
				
			||||||
			requestUser,
 | 
								requestUser,
 | 
				
			||||||
			existing,
 | 
								existing,
 | 
				
			||||||
			status,
 | 
								status,
 | 
				
			||||||
 | 
								mention,
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			log.Errorf(ctx, "failed to derive mention: %v", err)
 | 
								log.Errorf(ctx, "failed to derive mention: %v", err)
 | 
				
			||||||
| 
						 | 
					@ -845,7 +777,11 @@ func (d *Dereferencer) threadStatus(ctx context.Context, status *gtsmodel.Status
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (d *Dereferencer) fetchStatusTags(ctx context.Context, existing, status *gtsmodel.Status) error {
 | 
					func (d *Dereferencer) fetchStatusTags(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						existing *gtsmodel.Status,
 | 
				
			||||||
 | 
						status *gtsmodel.Status,
 | 
				
			||||||
 | 
					) error {
 | 
				
			||||||
	// Allocate new slice to take the yet-to-be determined tag IDs.
 | 
						// Allocate new slice to take the yet-to-be determined tag IDs.
 | 
				
			||||||
	status.TagIDs = make([]string, len(status.Tags))
 | 
						status.TagIDs = make([]string, len(status.Tags))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -900,7 +836,11 @@ func (d *Dereferencer) fetchStatusTags(ctx context.Context, existing, status *gt
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (d *Dereferencer) fetchStatusPoll(ctx context.Context, existing, status *gtsmodel.Status) error {
 | 
					func (d *Dereferencer) fetchStatusPoll(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						existing *gtsmodel.Status,
 | 
				
			||||||
 | 
						status *gtsmodel.Status,
 | 
				
			||||||
 | 
					) error {
 | 
				
			||||||
	var (
 | 
						var (
 | 
				
			||||||
		// insertStatusPoll generates ID and inserts the poll attached to status into the database.
 | 
							// insertStatusPoll generates ID and inserts the poll attached to status into the database.
 | 
				
			||||||
		insertStatusPoll = func(ctx context.Context, status *gtsmodel.Status) error {
 | 
							insertStatusPoll = func(ctx context.Context, status *gtsmodel.Status) error {
 | 
				
			||||||
| 
						 | 
					@ -990,19 +930,24 @@ func (d *Dereferencer) fetchStatusPoll(ctx context.Context, existing, status *gt
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (d *Dereferencer) fetchStatusAttachments(ctx context.Context, tsport transport.Transport, existing, status *gtsmodel.Status) error {
 | 
					func (d *Dereferencer) fetchStatusAttachments(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						requestUser string,
 | 
				
			||||||
 | 
						existing *gtsmodel.Status,
 | 
				
			||||||
 | 
						status *gtsmodel.Status,
 | 
				
			||||||
 | 
					) error {
 | 
				
			||||||
	// Allocate new slice to take the yet-to-be fetched attachment IDs.
 | 
						// Allocate new slice to take the yet-to-be fetched attachment IDs.
 | 
				
			||||||
	status.AttachmentIDs = make([]string, len(status.Attachments))
 | 
						status.AttachmentIDs = make([]string, len(status.Attachments))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for i := range status.Attachments {
 | 
						for i := range status.Attachments {
 | 
				
			||||||
		attachment := status.Attachments[i]
 | 
							placeholder := status.Attachments[i]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Look for existing media attachment with remote URL first.
 | 
							// Look for existing media attachment with remote URL first.
 | 
				
			||||||
		existing, ok := existing.GetAttachmentByRemoteURL(attachment.RemoteURL)
 | 
							existing, ok := existing.GetAttachmentByRemoteURL(placeholder.RemoteURL)
 | 
				
			||||||
		if ok && existing.ID != "" {
 | 
							if ok && existing.ID != "" {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// Ensure the existing media attachment is up-to-date and cached.
 | 
								// Ensure the existing media attachment is up-to-date and cached.
 | 
				
			||||||
			existing, err := d.updateAttachment(ctx, tsport, existing, attachment)
 | 
								existing, err := d.updateAttachment(ctx, requestUser, existing, placeholder)
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				log.Errorf(ctx, "error updating existing attachment: %v", err)
 | 
									log.Errorf(ctx, "error updating existing attachment: %v", err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1019,25 +964,25 @@ func (d *Dereferencer) fetchStatusAttachments(ctx context.Context, tsport transp
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Load this new media attachment.
 | 
							// Load this new media attachment.
 | 
				
			||||||
		attachment, err := d.loadAttachment(
 | 
							attachment, err := d.GetMedia(
 | 
				
			||||||
			ctx,
 | 
								ctx,
 | 
				
			||||||
			tsport,
 | 
								requestUser,
 | 
				
			||||||
			status.AccountID,
 | 
								status.AccountID,
 | 
				
			||||||
			attachment.RemoteURL,
 | 
								placeholder.RemoteURL,
 | 
				
			||||||
			&media.AdditionalMediaInfo{
 | 
								media.AdditionalMediaInfo{
 | 
				
			||||||
				StatusID:    &status.ID,
 | 
									StatusID:    &status.ID,
 | 
				
			||||||
				RemoteURL:   &attachment.RemoteURL,
 | 
									RemoteURL:   &placeholder.RemoteURL,
 | 
				
			||||||
				Description: &attachment.Description,
 | 
									Description: &placeholder.Description,
 | 
				
			||||||
				Blurhash:    &attachment.Blurhash,
 | 
									Blurhash:    &placeholder.Blurhash,
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
		if err != nil && attachment == nil {
 | 
							if err != nil {
 | 
				
			||||||
			log.Errorf(ctx, "error loading attachment: %v", err)
 | 
								if attachment == nil {
 | 
				
			||||||
 | 
									log.Errorf(ctx, "error loading attachment %s: %v", placeholder.RemoteURL, err)
 | 
				
			||||||
				continue
 | 
									continue
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if err != nil {
 | 
								// non-fatal error occurred during loading, still use it.
 | 
				
			||||||
			// A non-fatal error occurred during loading.
 | 
					 | 
				
			||||||
			log.Warnf(ctx, "partially loaded attachment: %v", err)
 | 
								log.Warnf(ctx, "partially loaded attachment: %v", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1061,22 +1006,108 @@ func (d *Dereferencer) fetchStatusAttachments(ctx context.Context, tsport transp
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (d *Dereferencer) fetchStatusEmojis(ctx context.Context, requestUser string, status *gtsmodel.Status) error {
 | 
					func (d *Dereferencer) fetchStatusEmojis(
 | 
				
			||||||
	// Fetch the full-fleshed-out emoji objects for our status.
 | 
						ctx context.Context,
 | 
				
			||||||
	emojis, err := d.populateEmojis(ctx, status.Emojis, requestUser)
 | 
						existing *gtsmodel.Status,
 | 
				
			||||||
 | 
						status *gtsmodel.Status,
 | 
				
			||||||
 | 
					) error {
 | 
				
			||||||
 | 
						// Fetch the updated emojis for our status.
 | 
				
			||||||
 | 
						emojis, changed, err := d.fetchEmojis(ctx,
 | 
				
			||||||
 | 
							existing.Emojis,
 | 
				
			||||||
 | 
							status.Emojis,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return gtserror.Newf("failed to populate emojis: %w", err)
 | 
							return gtserror.Newf("error fetching emojis: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Iterate over and get their IDs.
 | 
						if !changed {
 | 
				
			||||||
	emojiIDs := make([]string, 0, len(emojis))
 | 
							// Use existing status emoji objects.
 | 
				
			||||||
	for _, e := range emojis {
 | 
							status.EmojiIDs = existing.EmojiIDs
 | 
				
			||||||
		emojiIDs = append(emojiIDs, e.ID)
 | 
							status.Emojis = existing.Emojis
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Set known emoji details.
 | 
						// Set latest emojis.
 | 
				
			||||||
	status.Emojis = emojis
 | 
						status.Emojis = emojis
 | 
				
			||||||
	status.EmojiIDs = emojiIDs
 | 
					
 | 
				
			||||||
 | 
						// Iterate over and set changed emoji IDs.
 | 
				
			||||||
 | 
						status.EmojiIDs = make([]string, len(emojis))
 | 
				
			||||||
 | 
						for i, emoji := range emojis {
 | 
				
			||||||
 | 
							status.EmojiIDs[i] = emoji.ID
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// populateMentionTarget tries to populate the given
 | 
				
			||||||
 | 
					// mention with the correct TargetAccount and (if not
 | 
				
			||||||
 | 
					// yet set) TargetAccountURI, returning the populated
 | 
				
			||||||
 | 
					// mention.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Will check on the existing status if the mention
 | 
				
			||||||
 | 
					// is already there and populated; if so, existing
 | 
				
			||||||
 | 
					// mention will be returned along with `true`.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Otherwise, this function will try to parse first
 | 
				
			||||||
 | 
					// the Href of the mention, and then the namestring,
 | 
				
			||||||
 | 
					// to see who it targets, and go fetch that account.
 | 
				
			||||||
 | 
					func (d *Dereferencer) populateMentionTarget(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						requestUser string,
 | 
				
			||||||
 | 
						existing *gtsmodel.Status,
 | 
				
			||||||
 | 
						status *gtsmodel.Status,
 | 
				
			||||||
 | 
						mention *gtsmodel.Mention,
 | 
				
			||||||
 | 
					) (
 | 
				
			||||||
 | 
						*gtsmodel.Mention,
 | 
				
			||||||
 | 
						bool, // True if mention already exists in the DB.
 | 
				
			||||||
 | 
						error,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						// Mentions can be created using Name or Href.
 | 
				
			||||||
 | 
						// Prefer Href (TargetAccountURI), fall back to Name.
 | 
				
			||||||
 | 
						if mention.TargetAccountURI != "" {
 | 
				
			||||||
 | 
							// Look for existing mention with this URI.
 | 
				
			||||||
 | 
							// If we already have it we can return early.
 | 
				
			||||||
 | 
							existingMention, ok := existing.GetMentionByTargetURI(mention.TargetAccountURI)
 | 
				
			||||||
 | 
							if ok && existingMention.ID != "" {
 | 
				
			||||||
 | 
								return existingMention, true, nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Ensure that mention account URI is parseable.
 | 
				
			||||||
 | 
							accountURI, err := url.Parse(mention.TargetAccountURI)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								err = gtserror.Newf("invalid account uri %q: %w", mention.TargetAccountURI, err)
 | 
				
			||||||
 | 
								return nil, false, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Ensure we have the account of the mention target dereferenced.
 | 
				
			||||||
 | 
							mention.TargetAccount, _, err = d.getAccountByURI(ctx, requestUser, accountURI)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								err = gtserror.Newf("failed to dereference account %s: %w", accountURI, err)
 | 
				
			||||||
 | 
								return nil, false, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							// Href wasn't set. Find the target account using namestring.
 | 
				
			||||||
 | 
							username, domain, err := util.ExtractNamestringParts(mention.NameString)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								err = gtserror.Newf("failed to parse namestring %s: %w", mention.NameString, err)
 | 
				
			||||||
 | 
								return nil, false, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							mention.TargetAccount, _, err = d.getAccountByUsernameDomain(ctx, requestUser, username, domain)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								err = gtserror.Newf("failed to dereference account %s: %w", mention.NameString, err)
 | 
				
			||||||
 | 
								return nil, false, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Look for existing mention with this URI.
 | 
				
			||||||
 | 
							mention.TargetAccountURI = mention.TargetAccount.URI
 | 
				
			||||||
 | 
							existingMention, ok := existing.GetMentionByTargetURI(mention.TargetAccountURI)
 | 
				
			||||||
 | 
							if ok && existingMention.ID != "" {
 | 
				
			||||||
 | 
								return existingMention, true, nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// At this point, mention.TargetAccountURI
 | 
				
			||||||
 | 
						// and mention.TargetAccount must be set.
 | 
				
			||||||
 | 
						return mention, false, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,120 +18,36 @@
 | 
				
			||||||
package dereferencing
 | 
					package dereferencing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"io"
 | 
					 | 
				
			||||||
	"net/url"
 | 
					 | 
				
			||||||
	"slices"
 | 
						"slices"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
 | 
					 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/media"
 | 
					 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/transport"
 | 
					 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/util"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// loadAttachment handles the case of a new media attachment
 | 
					// getEmojiByShortcodeDomain searches input slice
 | 
				
			||||||
// that requires loading. it stores and caches from given data.
 | 
					// for emoji with given shortcode and domain.
 | 
				
			||||||
func (d *Dereferencer) loadAttachment(
 | 
					func getEmojiByShortcodeDomain(
 | 
				
			||||||
	ctx context.Context,
 | 
						emojis []*gtsmodel.Emoji,
 | 
				
			||||||
	tsport transport.Transport,
 | 
						shortcode string,
 | 
				
			||||||
	accountID string, // media account owner
 | 
						domain string,
 | 
				
			||||||
	remoteURL string,
 | 
					 | 
				
			||||||
	info *media.AdditionalMediaInfo,
 | 
					 | 
				
			||||||
) (
 | 
					) (
 | 
				
			||||||
	*gtsmodel.MediaAttachment,
 | 
						*gtsmodel.Emoji,
 | 
				
			||||||
	error,
 | 
						bool,
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
	// Parse str as valid URL object.
 | 
						for _, emoji := range emojis {
 | 
				
			||||||
	url, err := url.Parse(remoteURL)
 | 
							if emoji.Shortcode == shortcode &&
 | 
				
			||||||
	if err != nil {
 | 
								emoji.Domain == domain {
 | 
				
			||||||
		return nil, gtserror.Newf("invalid remote media url %q: %v", remoteURL, err)
 | 
								return emoji, true
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Start pre-processing remote media at remote URL.
 | 
					 | 
				
			||||||
	processing := d.mediaManager.PreProcessMedia(
 | 
					 | 
				
			||||||
		func(ctx context.Context) (io.ReadCloser, int64, error) {
 | 
					 | 
				
			||||||
			return tsport.DereferenceMedia(ctx, url)
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		accountID,
 | 
					 | 
				
			||||||
		info,
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Force attachment loading *right now*.
 | 
					 | 
				
			||||||
	return processing.LoadAttachment(ctx)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// updateAttachment handles the case of an existing media attachment
 | 
					 | 
				
			||||||
// that *may* have changes or need recaching. it checks for changed
 | 
					 | 
				
			||||||
// fields, updating in the database if so, and recaches uncached media.
 | 
					 | 
				
			||||||
func (d *Dereferencer) updateAttachment(
 | 
					 | 
				
			||||||
	ctx context.Context,
 | 
					 | 
				
			||||||
	tsport transport.Transport,
 | 
					 | 
				
			||||||
	existing *gtsmodel.MediaAttachment, // existing attachment
 | 
					 | 
				
			||||||
	media *gtsmodel.MediaAttachment, // (optional) changed media
 | 
					 | 
				
			||||||
) (
 | 
					 | 
				
			||||||
	*gtsmodel.MediaAttachment, // always set
 | 
					 | 
				
			||||||
	error,
 | 
					 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
	if media != nil {
 | 
					 | 
				
			||||||
		// Possible changed media columns.
 | 
					 | 
				
			||||||
		changed := make([]string, 0, 3)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Check if attachment description has changed.
 | 
					 | 
				
			||||||
		if existing.Description != media.Description {
 | 
					 | 
				
			||||||
			changed = append(changed, "description")
 | 
					 | 
				
			||||||
			existing.Description = media.Description
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Check if attachment blurhash has changed (i.e. content change).
 | 
					 | 
				
			||||||
		if existing.Blurhash != media.Blurhash && media.Blurhash != "" {
 | 
					 | 
				
			||||||
			changed = append(changed, "blurhash", "cached")
 | 
					 | 
				
			||||||
			existing.Blurhash = media.Blurhash
 | 
					 | 
				
			||||||
			existing.Cached = util.Ptr(false)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if len(changed) > 0 {
 | 
					 | 
				
			||||||
			// Update the existing attachment model in the database.
 | 
					 | 
				
			||||||
			err := d.state.DB.UpdateAttachment(ctx, existing, changed...)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return media, gtserror.Newf("error updating media: %w", err)
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						return nil, false
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Check if cached.
 | 
					// emojiChanged returns whether an emoji has changed in a way
 | 
				
			||||||
	if *existing.Cached {
 | 
					// that indicates that it should be refetched and refreshed.
 | 
				
			||||||
		return existing, nil
 | 
					func emojiChanged(existing, latest *gtsmodel.Emoji) bool {
 | 
				
			||||||
	}
 | 
						return existing.URI != latest.URI ||
 | 
				
			||||||
 | 
							existing.ImageRemoteURL != latest.ImageRemoteURL ||
 | 
				
			||||||
	// Parse str as valid URL object.
 | 
							existing.ImageStaticRemoteURL != latest.ImageStaticRemoteURL
 | 
				
			||||||
	url, err := url.Parse(existing.RemoteURL)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil, gtserror.Newf("invalid remote media url %q: %v", media.RemoteURL, err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Start pre-processing remote media recaching from remote.
 | 
					 | 
				
			||||||
	processing, err := d.mediaManager.PreProcessMediaRecache(
 | 
					 | 
				
			||||||
		ctx,
 | 
					 | 
				
			||||||
		func(ctx context.Context) (io.ReadCloser, int64, error) {
 | 
					 | 
				
			||||||
			return tsport.DereferenceMedia(ctx, url)
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		existing.ID,
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil, gtserror.Newf("error processing recache: %w", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Force load attachment recache *right now*.
 | 
					 | 
				
			||||||
	recached, err := processing.LoadAttachment(ctx)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Always return the error we
 | 
					 | 
				
			||||||
	// receive, but ensure we return
 | 
					 | 
				
			||||||
	// most up-to-date media file.
 | 
					 | 
				
			||||||
	if recached != nil {
 | 
					 | 
				
			||||||
		return recached, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return existing, err
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// pollChanged returns whether a poll has changed in way that
 | 
					// pollChanged returns whether a poll has changed in way that
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -30,19 +30,18 @@ type Emoji struct {
 | 
				
			||||||
	ImageStaticRemoteURL   string         `bun:",nullzero"`                                                   // Where can a static / non-animated version of this emoji be retrieved remotely? Null for local emojis.
 | 
						ImageStaticRemoteURL   string         `bun:",nullzero"`                                                   // Where can a static / non-animated version of this emoji be retrieved remotely? Null for local emojis.
 | 
				
			||||||
	ImageURL               string         `bun:",nullzero"`                                                   // Where can this emoji be retrieved from the local server? Null for remote emojis.
 | 
						ImageURL               string         `bun:",nullzero"`                                                   // Where can this emoji be retrieved from the local server? Null for remote emojis.
 | 
				
			||||||
	ImageStaticURL         string         `bun:",nullzero"`                                                   // Where can a static version of this emoji be retrieved from the local server? Null for remote emojis.
 | 
						ImageStaticURL         string         `bun:",nullzero"`                                                   // Where can a static version of this emoji be retrieved from the local server? Null for remote emojis.
 | 
				
			||||||
	ImagePath              string         `bun:",nullzero,notnull"`                                           // Path of the emoji image in the server storage system.
 | 
						ImagePath              string         `bun:",notnull"`                                                    // Path of the emoji image in the server storage system.
 | 
				
			||||||
	ImageStaticPath        string         `bun:",nullzero,notnull"`                                           // Path of a static version of the emoji image in the server storage system
 | 
						ImageStaticPath        string         `bun:",notnull"`                                                    // Path of a static version of the emoji image in the server storage system
 | 
				
			||||||
	ImageContentType       string         `bun:",nullzero,notnull"`                                           // MIME content type of the emoji image
 | 
						ImageContentType       string         `bun:",notnull"`                                                    // MIME content type of the emoji image
 | 
				
			||||||
	ImageStaticContentType string         `bun:",nullzero,notnull"`                                           // MIME content type of the static version of the emoji image.
 | 
						ImageStaticContentType string         `bun:",notnull"`                                                    // MIME content type of the static version of the emoji image.
 | 
				
			||||||
	ImageFileSize          int            `bun:",nullzero,notnull"`                                           // Size of the emoji image file in bytes, for serving purposes.
 | 
						ImageFileSize          int            `bun:",notnull"`                                                    // Size of the emoji image file in bytes, for serving purposes.
 | 
				
			||||||
	ImageStaticFileSize    int            `bun:",nullzero,notnull"`                                           // Size of the static version of the emoji image file in bytes, for serving purposes.
 | 
						ImageStaticFileSize    int            `bun:",notnull"`                                                    // Size of the static version of the emoji image file in bytes, for serving purposes.
 | 
				
			||||||
	ImageUpdatedAt         time.Time      `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // When was the emoji image last updated?
 | 
					 | 
				
			||||||
	Disabled               *bool          `bun:",nullzero,notnull,default:false"`                             // Has a moderation action disabled this emoji from being shown?
 | 
						Disabled               *bool          `bun:",nullzero,notnull,default:false"`                             // Has a moderation action disabled this emoji from being shown?
 | 
				
			||||||
	URI                    string         `bun:",nullzero,notnull,unique"`                                    // ActivityPub uri of this emoji. Something like 'https://example.org/emojis/1234'
 | 
						URI                    string         `bun:",nullzero,notnull,unique"`                                    // ActivityPub uri of this emoji. Something like 'https://example.org/emojis/1234'
 | 
				
			||||||
	VisibleInPicker        *bool          `bun:",nullzero,notnull,default:true"`                              // Is this emoji visible in the admin emoji picker?
 | 
						VisibleInPicker        *bool          `bun:",nullzero,notnull,default:true"`                              // Is this emoji visible in the admin emoji picker?
 | 
				
			||||||
	Category               *EmojiCategory `bun:"rel:belongs-to"`                                              // In which emoji category is this emoji visible?
 | 
						Category               *EmojiCategory `bun:"rel:belongs-to"`                                              // In which emoji category is this emoji visible?
 | 
				
			||||||
	CategoryID             string         `bun:"type:CHAR(26),nullzero"`                                      // ID of the category this emoji belongs to.
 | 
						CategoryID             string         `bun:"type:CHAR(26),nullzero"`                                      // ID of the category this emoji belongs to.
 | 
				
			||||||
	Cached                 *bool          `bun:",nullzero,notnull,default:false"`
 | 
						Cached                 *bool          `bun:",nullzero,notnull,default:false"`                             // whether emoji is cached in locally in gotosocial storage.
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// IsLocal returns true if the emoji is
 | 
					// IsLocal returns true if the emoji is
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -30,8 +30,8 @@ type MediaAttachment struct {
 | 
				
			||||||
	StatusID          string           `bun:"type:CHAR(26),nullzero"`                                      // ID of the status to which this is attached
 | 
						StatusID          string           `bun:"type:CHAR(26),nullzero"`                                      // ID of the status to which this is attached
 | 
				
			||||||
	URL               string           `bun:",nullzero"`                                                   // Where can the attachment be retrieved on *this* server
 | 
						URL               string           `bun:",nullzero"`                                                   // Where can the attachment be retrieved on *this* server
 | 
				
			||||||
	RemoteURL         string           `bun:",nullzero"`                                                   // Where can the attachment be retrieved on a remote server (empty for local media)
 | 
						RemoteURL         string           `bun:",nullzero"`                                                   // Where can the attachment be retrieved on a remote server (empty for local media)
 | 
				
			||||||
	Type              FileType         `bun:",nullzero,notnull"`                                           // Type of file (image/gifv/audio/video/unknown)
 | 
						Type              FileType         `bun:",notnull"`                                                    // Type of file (image/gifv/audio/video/unknown)
 | 
				
			||||||
	FileMeta          FileMeta         `bun:",embed:,nullzero,notnull"`                                    // Metadata about the file
 | 
						FileMeta          FileMeta         `bun:",embed:,notnull"`                                             // Metadata about the file
 | 
				
			||||||
	AccountID         string           `bun:"type:CHAR(26),nullzero,notnull"`                              // To which account does this attachment belong
 | 
						AccountID         string           `bun:"type:CHAR(26),nullzero,notnull"`                              // To which account does this attachment belong
 | 
				
			||||||
	Description       string           `bun:""`                                                            // Description of the attachment (for screenreaders)
 | 
						Description       string           `bun:""`                                                            // Description of the attachment (for screenreaders)
 | 
				
			||||||
	ScheduledStatusID string           `bun:"type:CHAR(26),nullzero"`                                      // To which scheduled status does this attachment belong
 | 
						ScheduledStatusID string           `bun:"type:CHAR(26),nullzero"`                                      // To which scheduled status does this attachment belong
 | 
				
			||||||
| 
						 | 
					@ -44,20 +44,28 @@ type MediaAttachment struct {
 | 
				
			||||||
	Cached            *bool            `bun:",nullzero,notnull,default:false"`                             // Is this attachment currently cached by our instance?
 | 
						Cached            *bool            `bun:",nullzero,notnull,default:false"`                             // Is this attachment currently cached by our instance?
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsLocal returns whether media attachment is local.
 | 
				
			||||||
 | 
					func (m *MediaAttachment) IsLocal() bool {
 | 
				
			||||||
 | 
						return m.RemoteURL == ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsRemote returns whether media attachment is remote.
 | 
				
			||||||
 | 
					func (m *MediaAttachment) IsRemote() bool {
 | 
				
			||||||
 | 
						return m.RemoteURL != ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// File refers to the metadata for the whole file
 | 
					// File refers to the metadata for the whole file
 | 
				
			||||||
type File struct {
 | 
					type File struct {
 | 
				
			||||||
	Path        string    `bun:",nullzero,notnull"`                                           // Path of the file in storage.
 | 
						Path        string `bun:",notnull"` // Path of the file in storage.
 | 
				
			||||||
	ContentType string    `bun:",nullzero,notnull"`                                           // MIME content type of the file.
 | 
						ContentType string `bun:",notnull"` // MIME content type of the file.
 | 
				
			||||||
	FileSize    int    `bun:",notnull"` // File size in bytes
 | 
						FileSize    int    `bun:",notnull"` // File size in bytes
 | 
				
			||||||
	UpdatedAt   time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // When was the file last updated.
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Thumbnail refers to a small image thumbnail derived from a larger image, video, or audio file.
 | 
					// Thumbnail refers to a small image thumbnail derived from a larger image, video, or audio file.
 | 
				
			||||||
type Thumbnail struct {
 | 
					type Thumbnail struct {
 | 
				
			||||||
	Path        string    `bun:",nullzero,notnull"`                                           // Path of the file in storage.
 | 
						Path        string `bun:",notnull"`  // Path of the file in storage.
 | 
				
			||||||
	ContentType string    `bun:",nullzero,notnull"`                                           // MIME content type of the file.
 | 
						ContentType string `bun:",notnull"`  // MIME content type of the file.
 | 
				
			||||||
	FileSize    int    `bun:",notnull"`  // File size in bytes
 | 
						FileSize    int    `bun:",notnull"`  // File size in bytes
 | 
				
			||||||
	UpdatedAt   time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // When was the file last updated.
 | 
					 | 
				
			||||||
	URL         string `bun:",nullzero"` // What is the URL of the thumbnail on the local server
 | 
						URL         string `bun:",nullzero"` // What is the URL of the thumbnail on the local server
 | 
				
			||||||
	RemoteURL   string `bun:",nullzero"` // What is the remote URL of the thumbnail (empty for local media)
 | 
						RemoteURL   string `bun:",nullzero"` // What is the remote URL of the thumbnail (empty for local media)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -43,12 +43,9 @@ var (
 | 
				
			||||||
		BufferPool:       &pngEncoderBufferPool{},
 | 
							BufferPool:       &pngEncoderBufferPool{},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// jpegBufferPool is a memory pool of byte buffers for JPEG encoding.
 | 
						// jpegBufferPool is a memory pool
 | 
				
			||||||
	jpegBufferPool = sync.Pool{
 | 
						// of byte buffers for JPEG encoding.
 | 
				
			||||||
		New: func() any {
 | 
						jpegBufferPool sync.Pool
 | 
				
			||||||
			return bufio.NewWriter(nil)
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// gtsImage is a thin wrapper around the standard library image
 | 
					// gtsImage is a thin wrapper around the standard library image
 | 
				
			||||||
| 
						 | 
					@ -80,25 +77,29 @@ func decodeImage(r io.Reader, opts ...imaging.DecodeOption) (*gtsImage, error) {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Width returns the image width in pixels.
 | 
					// Width returns the image width in pixels.
 | 
				
			||||||
func (m *gtsImage) Width() uint32 {
 | 
					func (m *gtsImage) Width() int {
 | 
				
			||||||
	return uint32(m.image.Bounds().Size().X)
 | 
						return m.image.Bounds().Size().X
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Height returns the image height in pixels.
 | 
					// Height returns the image height in pixels.
 | 
				
			||||||
func (m *gtsImage) Height() uint32 {
 | 
					func (m *gtsImage) Height() int {
 | 
				
			||||||
	return uint32(m.image.Bounds().Size().Y)
 | 
						return m.image.Bounds().Size().Y
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Size returns the total number of image pixels.
 | 
					// Size returns the total number of image pixels.
 | 
				
			||||||
func (m *gtsImage) Size() uint64 {
 | 
					func (m *gtsImage) Size() int {
 | 
				
			||||||
	return uint64(m.image.Bounds().Size().X) *
 | 
						return m.image.Bounds().Size().X *
 | 
				
			||||||
		uint64(m.image.Bounds().Size().Y)
 | 
							m.image.Bounds().Size().Y
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// AspectRatio returns the image ratio of width:height.
 | 
					// AspectRatio returns the image ratio of width:height.
 | 
				
			||||||
func (m *gtsImage) AspectRatio() float32 {
 | 
					func (m *gtsImage) AspectRatio() float32 {
 | 
				
			||||||
	return float32(m.image.Bounds().Size().X) /
 | 
					
 | 
				
			||||||
		float32(m.image.Bounds().Size().Y)
 | 
						// note: we cast bounds to float64 to prevent truncation
 | 
				
			||||||
 | 
						// and only at the end aspect ratio do we cast to float32
 | 
				
			||||||
 | 
						// (as the sizes are likely to be much larger than ratio).
 | 
				
			||||||
 | 
						return float32(float64(m.image.Bounds().Size().X) /
 | 
				
			||||||
 | 
							float64(m.image.Bounds().Size().Y))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Thumbnail returns a small sized copy of gtsImage{}, limited to 512x512 if not small enough.
 | 
					// Thumbnail returns a small sized copy of gtsImage{}, limited to 512x512 if not small enough.
 | 
				
			||||||
| 
						 | 
					@ -160,7 +161,11 @@ func (m *gtsImage) ToPNG() io.Reader {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// getJPEGBuffer fetches a reset JPEG encoding buffer from global JPEG buffer pool.
 | 
					// getJPEGBuffer fetches a reset JPEG encoding buffer from global JPEG buffer pool.
 | 
				
			||||||
func getJPEGBuffer(w io.Writer) *bufio.Writer {
 | 
					func getJPEGBuffer(w io.Writer) *bufio.Writer {
 | 
				
			||||||
	buf, _ := jpegBufferPool.Get().(*bufio.Writer)
 | 
						v := jpegBufferPool.Get()
 | 
				
			||||||
 | 
						if v == nil {
 | 
				
			||||||
 | 
							v = bufio.NewWriter(nil)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						buf := v.(*bufio.Writer)
 | 
				
			||||||
	buf.Reset(w)
 | 
						buf.Reset(w)
 | 
				
			||||||
	return buf
 | 
						return buf
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -56,176 +56,172 @@ func NewManager(state *state.State) *Manager {
 | 
				
			||||||
	return &Manager{state: state}
 | 
						return &Manager{state: state}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// PreProcessMedia begins the process of decoding
 | 
					// CreateMedia creates a new media attachment entry
 | 
				
			||||||
// and storing the given data as an attachment.
 | 
					// in the database for given owning account ID and
 | 
				
			||||||
// It will return a pointer to a ProcessingMedia
 | 
					// extra information, and prepares a new processing
 | 
				
			||||||
// struct upon which further actions can be performed,
 | 
					// media entry to dereference it using the given
 | 
				
			||||||
// such as getting the finished media, thumbnail,
 | 
					// data function, decode the media and finish filling
 | 
				
			||||||
// attachment, etc.
 | 
					// out remaining media fields (e.g. type, path, etc).
 | 
				
			||||||
//
 | 
					func (m *Manager) CreateMedia(
 | 
				
			||||||
//   - data: a function that the media manager can call
 | 
						ctx context.Context,
 | 
				
			||||||
//     to return a reader containing the media data.
 | 
					 | 
				
			||||||
//   - accountID: the account that the media belongs to.
 | 
					 | 
				
			||||||
//   - ai: optional and can be nil. Any additional information
 | 
					 | 
				
			||||||
//     about the attachment provided will be put in the database.
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// Note: unlike ProcessMedia, this will NOT
 | 
					 | 
				
			||||||
// queue the media to be asynchronously processed.
 | 
					 | 
				
			||||||
func (m *Manager) PreProcessMedia(
 | 
					 | 
				
			||||||
	data DataFunc,
 | 
					 | 
				
			||||||
	accountID string,
 | 
						accountID string,
 | 
				
			||||||
	ai *AdditionalMediaInfo,
 | 
						data DataFunc,
 | 
				
			||||||
) *ProcessingMedia {
 | 
						info AdditionalMediaInfo,
 | 
				
			||||||
 | 
					) (
 | 
				
			||||||
 | 
						*ProcessingMedia,
 | 
				
			||||||
 | 
						error,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						now := time.Now()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Generate new ID.
 | 
				
			||||||
 | 
						id := id.NewULID()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Placeholder URL for attachment.
 | 
				
			||||||
 | 
						url := uris.URIForAttachment(
 | 
				
			||||||
 | 
							accountID,
 | 
				
			||||||
 | 
							string(TypeAttachment),
 | 
				
			||||||
 | 
							string(SizeOriginal),
 | 
				
			||||||
 | 
							id,
 | 
				
			||||||
 | 
							"unknown",
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Placeholder storage path for attachment.
 | 
				
			||||||
 | 
						path := uris.StoragePathForAttachment(
 | 
				
			||||||
 | 
							accountID,
 | 
				
			||||||
 | 
							string(TypeAttachment),
 | 
				
			||||||
 | 
							string(SizeOriginal),
 | 
				
			||||||
 | 
							id,
 | 
				
			||||||
 | 
							"unknown",
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Calculate attachment thumbnail file path
 | 
				
			||||||
 | 
						thumbPath := uris.StoragePathForAttachment(
 | 
				
			||||||
 | 
							accountID,
 | 
				
			||||||
 | 
							string(TypeAttachment),
 | 
				
			||||||
 | 
							string(SizeSmall),
 | 
				
			||||||
 | 
							id,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Always encode attachment
 | 
				
			||||||
 | 
							// thumbnails as jpg.
 | 
				
			||||||
 | 
							"jpg",
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Calculate attachment thumbnail URL.
 | 
				
			||||||
 | 
						thumbURL := uris.URIForAttachment(
 | 
				
			||||||
 | 
							accountID,
 | 
				
			||||||
 | 
							string(TypeAttachment),
 | 
				
			||||||
 | 
							string(SizeSmall),
 | 
				
			||||||
 | 
							id,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Always encode attachment
 | 
				
			||||||
 | 
							// thumbnails as jpg.
 | 
				
			||||||
 | 
							"jpg",
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Populate initial fields on the new media,
 | 
						// Populate initial fields on the new media,
 | 
				
			||||||
	// leaving out fields with values we don't know
 | 
						// leaving out fields with values we don't know
 | 
				
			||||||
	// yet. These will be overwritten as we go.
 | 
						// yet. These will be overwritten as we go.
 | 
				
			||||||
	now := time.Now()
 | 
					 | 
				
			||||||
	attachment := >smodel.MediaAttachment{
 | 
						attachment := >smodel.MediaAttachment{
 | 
				
			||||||
		ID:         id.NewULID(),
 | 
							ID:         id,
 | 
				
			||||||
		CreatedAt:  now,
 | 
							CreatedAt:  now,
 | 
				
			||||||
		UpdatedAt:  now,
 | 
							UpdatedAt:  now,
 | 
				
			||||||
 | 
							URL:        url,
 | 
				
			||||||
		Type:       gtsmodel.FileTypeUnknown,
 | 
							Type:       gtsmodel.FileTypeUnknown,
 | 
				
			||||||
		FileMeta:   gtsmodel.FileMeta{},
 | 
					 | 
				
			||||||
		AccountID:  accountID,
 | 
							AccountID:  accountID,
 | 
				
			||||||
		Processing: gtsmodel.ProcessingStatusReceived,
 | 
							Processing: gtsmodel.ProcessingStatusReceived,
 | 
				
			||||||
		File: gtsmodel.File{
 | 
							File: gtsmodel.File{
 | 
				
			||||||
			UpdatedAt:   now,
 | 
					 | 
				
			||||||
			ContentType: "application/octet-stream",
 | 
								ContentType: "application/octet-stream",
 | 
				
			||||||
 | 
								Path:        path,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							Thumbnail: gtsmodel.Thumbnail{
 | 
				
			||||||
 | 
								ContentType: mimeImageJpeg, // thumbs always jpg.
 | 
				
			||||||
 | 
								Path:        thumbPath,
 | 
				
			||||||
 | 
								URL:         thumbURL,
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		Thumbnail: gtsmodel.Thumbnail{UpdatedAt: now},
 | 
					 | 
				
			||||||
		Avatar: util.Ptr(false),
 | 
							Avatar: util.Ptr(false),
 | 
				
			||||||
		Header: util.Ptr(false),
 | 
							Header: util.Ptr(false),
 | 
				
			||||||
		Cached: util.Ptr(false),
 | 
							Cached: util.Ptr(false),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	attachment.URL = uris.URIForAttachment(
 | 
					 | 
				
			||||||
		accountID,
 | 
					 | 
				
			||||||
		string(TypeAttachment),
 | 
					 | 
				
			||||||
		string(SizeOriginal),
 | 
					 | 
				
			||||||
		attachment.ID,
 | 
					 | 
				
			||||||
		"unknown",
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	attachment.File.Path = uris.StoragePathForAttachment(
 | 
					 | 
				
			||||||
		accountID,
 | 
					 | 
				
			||||||
		string(TypeAttachment),
 | 
					 | 
				
			||||||
		string(SizeOriginal),
 | 
					 | 
				
			||||||
		attachment.ID,
 | 
					 | 
				
			||||||
		"unknown",
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Check if we were provided additional info
 | 
						// Check if we were provided additional info
 | 
				
			||||||
	// to add to the attachment, and overwrite
 | 
						// to add to the attachment, and overwrite
 | 
				
			||||||
	// some of the attachment fields if so.
 | 
						// some of the attachment fields if so.
 | 
				
			||||||
	if ai != nil {
 | 
						if info.CreatedAt != nil {
 | 
				
			||||||
		if ai.CreatedAt != nil {
 | 
							attachment.CreatedAt = *info.CreatedAt
 | 
				
			||||||
			attachment.CreatedAt = *ai.CreatedAt
 | 
						}
 | 
				
			||||||
 | 
						if info.StatusID != nil {
 | 
				
			||||||
 | 
							attachment.StatusID = *info.StatusID
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if info.RemoteURL != nil {
 | 
				
			||||||
 | 
							attachment.RemoteURL = *info.RemoteURL
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if info.Description != nil {
 | 
				
			||||||
 | 
							attachment.Description = *info.Description
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if info.ScheduledStatusID != nil {
 | 
				
			||||||
 | 
							attachment.ScheduledStatusID = *info.ScheduledStatusID
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if info.Blurhash != nil {
 | 
				
			||||||
 | 
							attachment.Blurhash = *info.Blurhash
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if info.Avatar != nil {
 | 
				
			||||||
 | 
							attachment.Avatar = info.Avatar
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if info.Header != nil {
 | 
				
			||||||
 | 
							attachment.Header = info.Header
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if info.FocusX != nil {
 | 
				
			||||||
 | 
							attachment.FileMeta.Focus.X = *info.FocusX
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if info.FocusY != nil {
 | 
				
			||||||
 | 
							attachment.FileMeta.Focus.Y = *info.FocusY
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if ai.StatusID != nil {
 | 
						// Store attachment in database in initial form.
 | 
				
			||||||
			attachment.StatusID = *ai.StatusID
 | 
						err := m.state.DB.PutAttachment(ctx, attachment)
 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if ai.RemoteURL != nil {
 | 
					 | 
				
			||||||
			attachment.RemoteURL = *ai.RemoteURL
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if ai.Description != nil {
 | 
					 | 
				
			||||||
			attachment.Description = *ai.Description
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if ai.ScheduledStatusID != nil {
 | 
					 | 
				
			||||||
			attachment.ScheduledStatusID = *ai.ScheduledStatusID
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if ai.Blurhash != nil {
 | 
					 | 
				
			||||||
			attachment.Blurhash = *ai.Blurhash
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if ai.Avatar != nil {
 | 
					 | 
				
			||||||
			attachment.Avatar = ai.Avatar
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		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
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	processingMedia := &ProcessingMedia{
 | 
					 | 
				
			||||||
		media:  attachment,
 | 
					 | 
				
			||||||
		dataFn: data,
 | 
					 | 
				
			||||||
		mgr:    m,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return processingMedia
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// PreProcessMediaRecache refetches, reprocesses,
 | 
					 | 
				
			||||||
// and recaches an existing attachment that has
 | 
					 | 
				
			||||||
// been uncached via cleaner pruning.
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// Note: unlike ProcessMedia, this will NOT queue
 | 
					 | 
				
			||||||
// the media to be asychronously processed.
 | 
					 | 
				
			||||||
func (m *Manager) PreProcessMediaRecache(
 | 
					 | 
				
			||||||
	ctx context.Context,
 | 
					 | 
				
			||||||
	data DataFunc,
 | 
					 | 
				
			||||||
	attachmentID string,
 | 
					 | 
				
			||||||
) (*ProcessingMedia, error) {
 | 
					 | 
				
			||||||
	// Get the existing attachment from database.
 | 
					 | 
				
			||||||
	attachment, err := m.state.DB.GetAttachmentByID(ctx, attachmentID)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	processingMedia := &ProcessingMedia{
 | 
						// Pass prepared media as ready to be cached.
 | 
				
			||||||
		media:   attachment,
 | 
						return m.RecacheMedia(attachment, data), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RecacheMedia wraps a media model (assumed already
 | 
				
			||||||
 | 
					// inserted in the database!) with given data function
 | 
				
			||||||
 | 
					// to perform a blocking dereference / decode operation
 | 
				
			||||||
 | 
					// from the data stream returned.
 | 
				
			||||||
 | 
					func (m *Manager) RecacheMedia(
 | 
				
			||||||
 | 
						media *gtsmodel.MediaAttachment,
 | 
				
			||||||
 | 
						data DataFunc,
 | 
				
			||||||
 | 
					) *ProcessingMedia {
 | 
				
			||||||
 | 
						return &ProcessingMedia{
 | 
				
			||||||
 | 
							media:  media,
 | 
				
			||||||
		dataFn: data,
 | 
							dataFn: data,
 | 
				
			||||||
		recache: true, // Indicate it's a recache.
 | 
					 | 
				
			||||||
		mgr:    m,
 | 
							mgr:    m,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
	return processingMedia, nil
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// PreProcessEmoji begins the process of decoding and storing
 | 
					// CreateEmoji creates a new emoji entry in the
 | 
				
			||||||
// the given data as an emoji. It will return a pointer to a
 | 
					// database for given shortcode, domain and extra
 | 
				
			||||||
// ProcessingEmoji struct upon which further actions can be
 | 
					// information, and prepares a new processing emoji
 | 
				
			||||||
// performed, such as getting the finished media, thumbnail,
 | 
					// entry to dereference it using the given data
 | 
				
			||||||
// attachment, etc.
 | 
					// function, decode the media and finish filling
 | 
				
			||||||
//
 | 
					// out remaining fields (e.g. type, path, etc).
 | 
				
			||||||
//   - data: function that the media manager can call
 | 
					func (m *Manager) CreateEmoji(
 | 
				
			||||||
//     to return a reader containing the emoji data.
 | 
					 | 
				
			||||||
//   - shortcode: the emoji shortcode without the ':'s around it.
 | 
					 | 
				
			||||||
//   - emojiID: database ID that should be used to store the emoji.
 | 
					 | 
				
			||||||
//   - uri: ActivityPub URI/ID of the emoji.
 | 
					 | 
				
			||||||
//   - ai: optional and can be nil. Any additional information
 | 
					 | 
				
			||||||
//     about the emoji provided will be put in the database.
 | 
					 | 
				
			||||||
//   - refresh: refetch/refresh the emoji.
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// Note: unlike ProcessEmoji, this will NOT queue
 | 
					 | 
				
			||||||
// the emoji to be asynchronously processed.
 | 
					 | 
				
			||||||
func (m *Manager) PreProcessEmoji(
 | 
					 | 
				
			||||||
	ctx context.Context,
 | 
						ctx context.Context,
 | 
				
			||||||
	data DataFunc,
 | 
					 | 
				
			||||||
	shortcode string,
 | 
						shortcode string,
 | 
				
			||||||
	emojiID string,
 | 
						domain string,
 | 
				
			||||||
	uri string,
 | 
						data DataFunc,
 | 
				
			||||||
	ai *AdditionalEmojiInfo,
 | 
						info AdditionalEmojiInfo,
 | 
				
			||||||
	refresh bool,
 | 
					) (
 | 
				
			||||||
) (*ProcessingEmoji, error) {
 | 
						*ProcessingEmoji,
 | 
				
			||||||
	var (
 | 
						error,
 | 
				
			||||||
		newPathID string
 | 
					) {
 | 
				
			||||||
		emoji     *gtsmodel.Emoji
 | 
						now := time.Now()
 | 
				
			||||||
		now       = time.Now()
 | 
					
 | 
				
			||||||
	)
 | 
						// Generate new ID.
 | 
				
			||||||
 | 
						id := id.NewULID()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Fetch the local instance account for emoji path generation.
 | 
						// Fetch the local instance account for emoji path generation.
 | 
				
			||||||
	instanceAcc, err := m.state.DB.GetInstanceAccount(ctx, "")
 | 
						instanceAcc, err := m.state.DB.GetInstanceAccount(ctx, "")
 | 
				
			||||||
| 
						 | 
					@ -233,95 +229,31 @@ func (m *Manager) PreProcessEmoji(
 | 
				
			||||||
		return nil, gtserror.Newf("error fetching instance account: %w", err)
 | 
							return nil, gtserror.Newf("error fetching instance account: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if refresh {
 | 
						if domain == "" && info.URI == nil {
 | 
				
			||||||
		// Existing emoji!
 | 
							// Generate URI for local emoji.
 | 
				
			||||||
 | 
							uri := uris.URIForEmoji(id)
 | 
				
			||||||
		emoji, err = m.state.DB.GetEmojiByID(ctx, emojiID)
 | 
							info.URI = &uri
 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			err = gtserror.Newf("error fetching emoji to refresh from the db: %w", err)
 | 
					 | 
				
			||||||
			return nil, err
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Since this is a refresh, we will end up with
 | 
						// Generate static URL for attachment.
 | 
				
			||||||
		// new images stored for this emoji, so we should
 | 
						staticURL := uris.URIForAttachment(
 | 
				
			||||||
		// use an io.Closer callback to perform clean up
 | 
					 | 
				
			||||||
		// of the original images from storage.
 | 
					 | 
				
			||||||
		originalData := data
 | 
					 | 
				
			||||||
		originalImagePath := emoji.ImagePath
 | 
					 | 
				
			||||||
		originalImageStaticPath := emoji.ImageStaticPath
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		data = func(ctx context.Context) (io.ReadCloser, int64, error) {
 | 
					 | 
				
			||||||
			// Call original data func.
 | 
					 | 
				
			||||||
			rc, sz, err := originalData(ctx)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return nil, 0, err
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Wrap closer to cleanup old data.
 | 
					 | 
				
			||||||
			c := iotools.CloserCallback(rc, func() {
 | 
					 | 
				
			||||||
				if err := m.state.Storage.Delete(ctx, originalImagePath); err != nil && !storage.IsNotFound(err) {
 | 
					 | 
				
			||||||
					log.Errorf(ctx, "error removing old emoji %s@%s from storage: %v", emoji.Shortcode, emoji.Domain, err)
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				if err := m.state.Storage.Delete(ctx, originalImageStaticPath); err != nil && !storage.IsNotFound(err) {
 | 
					 | 
				
			||||||
					log.Errorf(ctx, "error removing old static emoji %s@%s from storage: %v", emoji.Shortcode, emoji.Domain, err)
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Return newly wrapped readcloser and size.
 | 
					 | 
				
			||||||
			return iotools.ReadCloser(rc, c), sz, nil
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Reuse existing shortcode and URI -
 | 
					 | 
				
			||||||
		// these don't change when we refresh.
 | 
					 | 
				
			||||||
		emoji.Shortcode = shortcode
 | 
					 | 
				
			||||||
		emoji.URI = uri
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Use a new ID to create a new path
 | 
					 | 
				
			||||||
		// for the new images, to get around
 | 
					 | 
				
			||||||
		// needing to do cache invalidation.
 | 
					 | 
				
			||||||
		newPathID, err = id.NewRandomULID()
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return nil, gtserror.Newf("error generating alternateID for emoji refresh: %s", err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		emoji.ImageStaticURL = uris.URIForAttachment(
 | 
					 | 
				
			||||||
		instanceAcc.ID,
 | 
							instanceAcc.ID,
 | 
				
			||||||
		string(TypeEmoji),
 | 
							string(TypeEmoji),
 | 
				
			||||||
		string(SizeStatic),
 | 
							string(SizeStatic),
 | 
				
			||||||
			newPathID,
 | 
							id,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// All static emojis
 | 
							// All static emojis
 | 
				
			||||||
		// are encoded as png.
 | 
							// are encoded as png.
 | 
				
			||||||
		mimePng,
 | 
							mimePng,
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		emoji.ImageStaticPath = uris.StoragePathForAttachment(
 | 
						// Generate static image path for attachment.
 | 
				
			||||||
 | 
						staticPath := uris.StoragePathForAttachment(
 | 
				
			||||||
		instanceAcc.ID,
 | 
							instanceAcc.ID,
 | 
				
			||||||
		string(TypeEmoji),
 | 
							string(TypeEmoji),
 | 
				
			||||||
		string(SizeStatic),
 | 
							string(SizeStatic),
 | 
				
			||||||
			newPathID,
 | 
							id,
 | 
				
			||||||
			// All static emojis
 | 
					 | 
				
			||||||
			// are encoded as png.
 | 
					 | 
				
			||||||
			mimePng,
 | 
					 | 
				
			||||||
		)
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		// New emoji!
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		imageStaticURL := uris.URIForAttachment(
 | 
					 | 
				
			||||||
			instanceAcc.ID,
 | 
					 | 
				
			||||||
			string(TypeEmoji),
 | 
					 | 
				
			||||||
			string(SizeStatic),
 | 
					 | 
				
			||||||
			emojiID,
 | 
					 | 
				
			||||||
			// All static emojis
 | 
					 | 
				
			||||||
			// are encoded as png.
 | 
					 | 
				
			||||||
			mimePng,
 | 
					 | 
				
			||||||
		)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		imageStaticPath := uris.StoragePathForAttachment(
 | 
					 | 
				
			||||||
			instanceAcc.ID,
 | 
					 | 
				
			||||||
			string(TypeEmoji),
 | 
					 | 
				
			||||||
			string(SizeStatic),
 | 
					 | 
				
			||||||
			emojiID,
 | 
					 | 
				
			||||||
		// All static emojis
 | 
							// All static emojis
 | 
				
			||||||
		// are encoded as png.
 | 
							// are encoded as png.
 | 
				
			||||||
		mimePng,
 | 
							mimePng,
 | 
				
			||||||
| 
						 | 
					@ -330,57 +262,189 @@ func (m *Manager) PreProcessEmoji(
 | 
				
			||||||
	// Populate initial fields on the new emoji,
 | 
						// Populate initial fields on the new emoji,
 | 
				
			||||||
	// leaving out fields with values we don't know
 | 
						// leaving out fields with values we don't know
 | 
				
			||||||
	// yet. These will be overwritten as we go.
 | 
						// yet. These will be overwritten as we go.
 | 
				
			||||||
		emoji = >smodel.Emoji{
 | 
						emoji := >smodel.Emoji{
 | 
				
			||||||
			ID:                     emojiID,
 | 
							ID:                     id,
 | 
				
			||||||
 | 
							Shortcode:              shortcode,
 | 
				
			||||||
 | 
							Domain:                 domain,
 | 
				
			||||||
 | 
							ImageStaticURL:         staticURL,
 | 
				
			||||||
 | 
							ImageStaticPath:        staticPath,
 | 
				
			||||||
 | 
							ImageStaticContentType: mimeImagePng,
 | 
				
			||||||
 | 
							Disabled:               util.Ptr(false),
 | 
				
			||||||
 | 
							VisibleInPicker:        util.Ptr(true),
 | 
				
			||||||
		CreatedAt:              now,
 | 
							CreatedAt:              now,
 | 
				
			||||||
		UpdatedAt:              now,
 | 
							UpdatedAt:              now,
 | 
				
			||||||
			Shortcode:              shortcode,
 | 
					 | 
				
			||||||
			ImageStaticURL:         imageStaticURL,
 | 
					 | 
				
			||||||
			ImageStaticPath:        imageStaticPath,
 | 
					 | 
				
			||||||
			ImageStaticContentType: mimeImagePng,
 | 
					 | 
				
			||||||
			ImageUpdatedAt:         now,
 | 
					 | 
				
			||||||
			Disabled:               util.Ptr(false),
 | 
					 | 
				
			||||||
			URI:                    uri,
 | 
					 | 
				
			||||||
			VisibleInPicker:        util.Ptr(true),
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Finally, create new emoji.
 | 
				
			||||||
 | 
						return m.createEmoji(ctx,
 | 
				
			||||||
 | 
							m.state.DB.PutEmoji,
 | 
				
			||||||
 | 
							data,
 | 
				
			||||||
 | 
							emoji,
 | 
				
			||||||
 | 
							info,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RefreshEmoji will prepare a recache operation
 | 
				
			||||||
 | 
					// for the given emoji, updating it with extra
 | 
				
			||||||
 | 
					// information, and in particular using new storage
 | 
				
			||||||
 | 
					// paths for the dereferenced media files to skirt
 | 
				
			||||||
 | 
					// around browser caching of the old files.
 | 
				
			||||||
 | 
					func (m *Manager) RefreshEmoji(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						emoji *gtsmodel.Emoji,
 | 
				
			||||||
 | 
						data DataFunc,
 | 
				
			||||||
 | 
						info AdditionalEmojiInfo,
 | 
				
			||||||
 | 
					) (
 | 
				
			||||||
 | 
						*ProcessingEmoji,
 | 
				
			||||||
 | 
						error,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						// Fetch the local instance account for emoji path generation.
 | 
				
			||||||
 | 
						instanceAcc, err := m.state.DB.GetInstanceAccount(ctx, "")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, gtserror.Newf("error fetching instance account: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create references to old emoji image
 | 
				
			||||||
 | 
						// paths before they get updated with new
 | 
				
			||||||
 | 
						// path ID. These are required for later
 | 
				
			||||||
 | 
						// deleting the old image files on refresh.
 | 
				
			||||||
 | 
						shortcodeDomain := util.ShortcodeDomain(emoji)
 | 
				
			||||||
 | 
						oldStaticPath := emoji.ImageStaticPath
 | 
				
			||||||
 | 
						oldPath := emoji.ImagePath
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Since this is a refresh we will end up storing new images at new
 | 
				
			||||||
 | 
						// paths, so we should wrap closer to delete old paths at completion.
 | 
				
			||||||
 | 
						wrapped := func(ctx context.Context) (io.ReadCloser, int64, error) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Call original data func.
 | 
				
			||||||
 | 
							rc, sz, err := data(ctx)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, 0, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Wrap closer to cleanup old data.
 | 
				
			||||||
 | 
							c := iotools.CloserFunc(func() error {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// First try close original.
 | 
				
			||||||
 | 
								if rc.Close(); err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Remove any *old* emoji image file path now stream is closed.
 | 
				
			||||||
 | 
								if err := m.state.Storage.Delete(ctx, oldPath); err != nil &&
 | 
				
			||||||
 | 
									!storage.IsNotFound(err) {
 | 
				
			||||||
 | 
									log.Errorf(ctx, "error deleting old emoji %s from storage: %v", shortcodeDomain, err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Remove any *old* emoji static image file path now stream is closed.
 | 
				
			||||||
 | 
								if err := m.state.Storage.Delete(ctx, oldStaticPath); err != nil &&
 | 
				
			||||||
 | 
									!storage.IsNotFound(err) {
 | 
				
			||||||
 | 
									log.Errorf(ctx, "error deleting old static emoji %s from storage: %v", shortcodeDomain, err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Return newly wrapped readcloser and size.
 | 
				
			||||||
 | 
							return iotools.ReadCloser(rc, c), sz, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Use a new ID to create a new path
 | 
				
			||||||
 | 
						// for the new images, to get around
 | 
				
			||||||
 | 
						// needing to do cache invalidation.
 | 
				
			||||||
 | 
						newPathID, err := id.NewRandomULID()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, gtserror.Newf("error generating newPathID for emoji refresh: %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Generate new static URL for emoji.
 | 
				
			||||||
 | 
						emoji.ImageStaticURL = uris.URIForAttachment(
 | 
				
			||||||
 | 
							instanceAcc.ID,
 | 
				
			||||||
 | 
							string(TypeEmoji),
 | 
				
			||||||
 | 
							string(SizeStatic),
 | 
				
			||||||
 | 
							newPathID,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// All static emojis
 | 
				
			||||||
 | 
							// are encoded as png.
 | 
				
			||||||
 | 
							mimePng,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Generate new static image storage path for emoji.
 | 
				
			||||||
 | 
						emoji.ImageStaticPath = uris.StoragePathForAttachment(
 | 
				
			||||||
 | 
							instanceAcc.ID,
 | 
				
			||||||
 | 
							string(TypeEmoji),
 | 
				
			||||||
 | 
							string(SizeStatic),
 | 
				
			||||||
 | 
							newPathID,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// All static emojis
 | 
				
			||||||
 | 
							// are encoded as png.
 | 
				
			||||||
 | 
							mimePng,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Finally, create new emoji in database.
 | 
				
			||||||
 | 
						processingEmoji, err := m.createEmoji(ctx,
 | 
				
			||||||
 | 
							func(ctx context.Context, emoji *gtsmodel.Emoji) error {
 | 
				
			||||||
 | 
								return m.state.DB.UpdateEmoji(ctx, emoji)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							wrapped,
 | 
				
			||||||
 | 
							emoji,
 | 
				
			||||||
 | 
							info,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Set the refreshed path ID used.
 | 
				
			||||||
 | 
						processingEmoji.newPathID = newPathID
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return processingEmoji, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m *Manager) createEmoji(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						putDB func(context.Context, *gtsmodel.Emoji) error,
 | 
				
			||||||
 | 
						data DataFunc,
 | 
				
			||||||
 | 
						emoji *gtsmodel.Emoji,
 | 
				
			||||||
 | 
						info AdditionalEmojiInfo,
 | 
				
			||||||
 | 
					) (
 | 
				
			||||||
 | 
						*ProcessingEmoji,
 | 
				
			||||||
 | 
						error,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
	// Check if we have additional info to add to the emoji,
 | 
						// Check if we have additional info to add to the emoji,
 | 
				
			||||||
	// and overwrite some of the emoji fields if so.
 | 
						// and overwrite some of the emoji fields if so.
 | 
				
			||||||
	if ai != nil {
 | 
						if info.URI != nil {
 | 
				
			||||||
		if ai.CreatedAt != nil {
 | 
							emoji.URI = *info.URI
 | 
				
			||||||
			emoji.CreatedAt = *ai.CreatedAt
 | 
						}
 | 
				
			||||||
 | 
						if info.CreatedAt != nil {
 | 
				
			||||||
 | 
							emoji.CreatedAt = *info.CreatedAt
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if info.Domain != nil {
 | 
				
			||||||
 | 
							emoji.Domain = *info.Domain
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if info.ImageRemoteURL != nil {
 | 
				
			||||||
 | 
							emoji.ImageRemoteURL = *info.ImageRemoteURL
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if info.ImageStaticRemoteURL != nil {
 | 
				
			||||||
 | 
							emoji.ImageStaticRemoteURL = *info.ImageStaticRemoteURL
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if info.Disabled != nil {
 | 
				
			||||||
 | 
							emoji.Disabled = info.Disabled
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if info.VisibleInPicker != nil {
 | 
				
			||||||
 | 
							emoji.VisibleInPicker = info.VisibleInPicker
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if info.CategoryID != nil {
 | 
				
			||||||
 | 
							emoji.CategoryID = *info.CategoryID
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if ai.Domain != nil {
 | 
						// Store emoji in database in initial form.
 | 
				
			||||||
			emoji.Domain = *ai.Domain
 | 
						if err := putDB(ctx, emoji); err != nil {
 | 
				
			||||||
		}
 | 
							return nil, err
 | 
				
			||||||
 | 
					 | 
				
			||||||
		if ai.ImageRemoteURL != nil {
 | 
					 | 
				
			||||||
			emoji.ImageRemoteURL = *ai.ImageRemoteURL
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if ai.ImageStaticRemoteURL != nil {
 | 
					 | 
				
			||||||
			emoji.ImageStaticRemoteURL = *ai.ImageStaticRemoteURL
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if ai.Disabled != nil {
 | 
					 | 
				
			||||||
			emoji.Disabled = ai.Disabled
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if ai.VisibleInPicker != nil {
 | 
					 | 
				
			||||||
			emoji.VisibleInPicker = ai.VisibleInPicker
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if ai.CategoryID != nil {
 | 
					 | 
				
			||||||
			emoji.CategoryID = *ai.CategoryID
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Return wrapped emoji for later processing.
 | 
				
			||||||
	processingEmoji := &ProcessingEmoji{
 | 
						processingEmoji := &ProcessingEmoji{
 | 
				
			||||||
		emoji:  emoji,
 | 
							emoji:  emoji,
 | 
				
			||||||
		existing:  refresh,
 | 
					 | 
				
			||||||
		newPathID: newPathID,
 | 
					 | 
				
			||||||
		dataFn: data,
 | 
							dataFn: data,
 | 
				
			||||||
		mgr:    m,
 | 
							mgr:    m,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -388,51 +452,17 @@ func (m *Manager) PreProcessEmoji(
 | 
				
			||||||
	return processingEmoji, nil
 | 
						return processingEmoji, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// PreProcessEmojiRecache refetches, reprocesses, and recaches
 | 
					// RecacheEmoji wraps an emoji model (assumed already
 | 
				
			||||||
// an existing emoji that has been uncached via cleaner pruning.
 | 
					// inserted in the database!) with given data function
 | 
				
			||||||
//
 | 
					// to perform a blocking dereference / decode operation
 | 
				
			||||||
// Note: unlike ProcessEmoji, this will NOT queue the emoji to
 | 
					// from the data stream returned.
 | 
				
			||||||
// be asychronously processed.
 | 
					func (m *Manager) RecacheEmoji(
 | 
				
			||||||
func (m *Manager) PreProcessEmojiRecache(
 | 
						emoji *gtsmodel.Emoji,
 | 
				
			||||||
	ctx context.Context,
 | 
					 | 
				
			||||||
	data DataFunc,
 | 
						data DataFunc,
 | 
				
			||||||
	emojiID string,
 | 
					) *ProcessingEmoji {
 | 
				
			||||||
) (*ProcessingEmoji, error) {
 | 
						return &ProcessingEmoji{
 | 
				
			||||||
	// Get the existing emoji from the database.
 | 
					 | 
				
			||||||
	emoji, err := m.state.DB.GetEmojiByID(ctx, emojiID)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	processingEmoji := &ProcessingEmoji{
 | 
					 | 
				
			||||||
		emoji:  emoji,
 | 
							emoji:  emoji,
 | 
				
			||||||
		dataFn: data,
 | 
							dataFn: data,
 | 
				
			||||||
		existing: true, // Indicate recache.
 | 
					 | 
				
			||||||
		mgr:    m,
 | 
							mgr:    m,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
	return processingEmoji, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// ProcessEmoji will call PreProcessEmoji, followed
 | 
					 | 
				
			||||||
// by queuing the emoji in the emoji worker queue.
 | 
					 | 
				
			||||||
func (m *Manager) ProcessEmoji(
 | 
					 | 
				
			||||||
	ctx context.Context,
 | 
					 | 
				
			||||||
	data DataFunc,
 | 
					 | 
				
			||||||
	shortcode string,
 | 
					 | 
				
			||||||
	id string,
 | 
					 | 
				
			||||||
	uri string,
 | 
					 | 
				
			||||||
	ai *AdditionalEmojiInfo,
 | 
					 | 
				
			||||||
	refresh bool,
 | 
					 | 
				
			||||||
) (*ProcessingEmoji, error) {
 | 
					 | 
				
			||||||
	// Create a new processing emoji object for this emoji request.
 | 
					 | 
				
			||||||
	emoji, err := m.PreProcessEmoji(ctx, data, shortcode, id, uri, ai, refresh)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Attempt to add emoji item to the worker queue.
 | 
					 | 
				
			||||||
	m.state.Workers.Media.Queue.Push(emoji.Process)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return emoji, nil
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -40,7 +40,7 @@ type ManagerTestSuite struct {
 | 
				
			||||||
	MediaStandardTestSuite
 | 
						MediaStandardTestSuite
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (suite *ManagerTestSuite) TestEmojiProcessBlocking() {
 | 
					func (suite *ManagerTestSuite) TestEmojiProcess() {
 | 
				
			||||||
	ctx := context.Background()
 | 
						ctx := context.Background()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
						data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
				
			||||||
| 
						 | 
					@ -52,27 +52,26 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlocking() {
 | 
				
			||||||
		return io.NopCloser(bytes.NewBuffer(b)), int64(len(b)), nil
 | 
							return io.NopCloser(bytes.NewBuffer(b)), int64(len(b)), nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	emojiID := "01GDQ9G782X42BAMFASKP64343"
 | 
						processing, err := suite.manager.CreateEmoji(ctx,
 | 
				
			||||||
	emojiURI := "http://localhost:8080/emoji/01GDQ9G782X42BAMFASKP64343"
 | 
							"rainbow_test",
 | 
				
			||||||
 | 
							"",
 | 
				
			||||||
	processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, "rainbow_test", emojiID, emojiURI, nil, false)
 | 
							data,
 | 
				
			||||||
 | 
							media.AdditionalEmojiInfo{},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// do a blocking call to fetch the emoji
 | 
						// do a blocking call to fetch the emoji
 | 
				
			||||||
	emoji, err := processingEmoji.LoadEmoji(ctx)
 | 
						emoji, err := processing.Load(ctx)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(emoji)
 | 
						suite.NotNil(emoji)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// make sure it's got the stuff set on it that we expect
 | 
					 | 
				
			||||||
	suite.Equal(emojiID, emoji.ID)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// file meta should be correctly derived from the image
 | 
						// file meta should be correctly derived from the image
 | 
				
			||||||
	suite.Equal("image/png", emoji.ImageContentType)
 | 
						suite.Equal("image/png", emoji.ImageContentType)
 | 
				
			||||||
	suite.Equal("image/png", emoji.ImageStaticContentType)
 | 
						suite.Equal("image/png", emoji.ImageStaticContentType)
 | 
				
			||||||
	suite.Equal(36702, emoji.ImageFileSize)
 | 
						suite.Equal(36702, emoji.ImageFileSize)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// now make sure the emoji is in the database
 | 
						// now make sure the emoji is in the database
 | 
				
			||||||
	dbEmoji, err := suite.db.GetEmojiByID(ctx, emojiID)
 | 
						dbEmoji, err := suite.db.GetEmojiByID(ctx, emoji.ID)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(dbEmoji)
 | 
						suite.NotNil(dbEmoji)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -101,14 +100,15 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlocking() {
 | 
				
			||||||
	suite.Equal(processedStaticBytesExpected, processedStaticBytes)
 | 
						suite.Equal(processedStaticBytesExpected, processedStaticBytes)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (suite *ManagerTestSuite) TestEmojiProcessBlockingRefresh() {
 | 
					func (suite *ManagerTestSuite) TestEmojiProcessRefresh() {
 | 
				
			||||||
	ctx := context.Background()
 | 
						ctx := context.Background()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// we're going to 'refresh' the remote 'yell' emoji by changing the image url to the pixellated gts logo
 | 
						// we're going to 'refresh' the remote 'yell' emoji by changing the image url to the pixellated gts logo
 | 
				
			||||||
	originalEmoji := suite.testEmojis["yell"]
 | 
						originalEmoji := suite.testEmojis["yell"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	emojiToUpdate := >smodel.Emoji{}
 | 
						emojiToUpdate, err := suite.db.GetEmojiByID(ctx, originalEmoji.ID)
 | 
				
			||||||
	*emojiToUpdate = *originalEmoji
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	newImageRemoteURL := "http://fossbros-anonymous.io/some/image/path.png"
 | 
						newImageRemoteURL := "http://fossbros-anonymous.io/some/image/path.png"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	oldEmojiImagePath := emojiToUpdate.ImagePath
 | 
						oldEmojiImagePath := emojiToUpdate.ImagePath
 | 
				
			||||||
| 
						 | 
					@ -122,23 +122,24 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlockingRefresh() {
 | 
				
			||||||
		return io.NopCloser(bytes.NewBuffer(b)), int64(len(b)), nil
 | 
							return io.NopCloser(bytes.NewBuffer(b)), int64(len(b)), nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	emojiID := emojiToUpdate.ID
 | 
						processing, err := suite.manager.RefreshEmoji(ctx,
 | 
				
			||||||
	emojiURI := emojiToUpdate.URI
 | 
							emojiToUpdate,
 | 
				
			||||||
 | 
							data,
 | 
				
			||||||
	processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, "yell", emojiID, emojiURI, &media.AdditionalEmojiInfo{
 | 
							media.AdditionalEmojiInfo{
 | 
				
			||||||
			CreatedAt:      &emojiToUpdate.CreatedAt,
 | 
								CreatedAt:      &emojiToUpdate.CreatedAt,
 | 
				
			||||||
			Domain:         &emojiToUpdate.Domain,
 | 
								Domain:         &emojiToUpdate.Domain,
 | 
				
			||||||
			ImageRemoteURL: &newImageRemoteURL,
 | 
								ImageRemoteURL: &newImageRemoteURL,
 | 
				
			||||||
	}, true)
 | 
							},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// do a blocking call to fetch the emoji
 | 
						// do a blocking call to fetch the emoji
 | 
				
			||||||
	emoji, err := processingEmoji.LoadEmoji(ctx)
 | 
						emoji, err := processing.Load(ctx)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(emoji)
 | 
						suite.NotNil(emoji)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// make sure it's got the stuff set on it that we expect
 | 
						// make sure it's got the stuff set on it that we expect
 | 
				
			||||||
	suite.Equal(emojiID, emoji.ID)
 | 
						suite.Equal(originalEmoji.ID, emoji.ID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// file meta should be correctly derived from the image
 | 
						// file meta should be correctly derived from the image
 | 
				
			||||||
	suite.Equal("image/png", emoji.ImageContentType)
 | 
						suite.Equal("image/png", emoji.ImageContentType)
 | 
				
			||||||
| 
						 | 
					@ -146,7 +147,7 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlockingRefresh() {
 | 
				
			||||||
	suite.Equal(10296, emoji.ImageFileSize)
 | 
						suite.Equal(10296, emoji.ImageFileSize)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// now make sure the emoji is in the database
 | 
						// now make sure the emoji is in the database
 | 
				
			||||||
	dbEmoji, err := suite.db.GetEmojiByID(ctx, emojiID)
 | 
						dbEmoji, err := suite.db.GetEmojiByID(ctx, emoji.ID)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(dbEmoji)
 | 
						suite.NotNil(dbEmoji)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -185,7 +186,6 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlockingRefresh() {
 | 
				
			||||||
	suite.NotEqual(originalEmoji.ImageStaticPath, dbEmoji.ImageStaticPath)
 | 
						suite.NotEqual(originalEmoji.ImageStaticPath, dbEmoji.ImageStaticPath)
 | 
				
			||||||
	suite.NotEqual(originalEmoji.ImageStaticPath, dbEmoji.ImageStaticPath)
 | 
						suite.NotEqual(originalEmoji.ImageStaticPath, dbEmoji.ImageStaticPath)
 | 
				
			||||||
	suite.NotEqual(originalEmoji.UpdatedAt, dbEmoji.UpdatedAt)
 | 
						suite.NotEqual(originalEmoji.UpdatedAt, dbEmoji.UpdatedAt)
 | 
				
			||||||
	suite.NotEqual(originalEmoji.ImageUpdatedAt, dbEmoji.ImageUpdatedAt)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// the old image files should no longer be in storage
 | 
						// the old image files should no longer be in storage
 | 
				
			||||||
	_, err = suite.storage.Get(ctx, oldEmojiImagePath)
 | 
						_, err = suite.storage.Get(ctx, oldEmojiImagePath)
 | 
				
			||||||
| 
						 | 
					@ -194,7 +194,7 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlockingRefresh() {
 | 
				
			||||||
	suite.True(storage.IsNotFound(err))
 | 
						suite.True(storage.IsNotFound(err))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (suite *ManagerTestSuite) TestEmojiProcessBlockingTooLarge() {
 | 
					func (suite *ManagerTestSuite) TestEmojiProcessTooLarge() {
 | 
				
			||||||
	ctx := context.Background()
 | 
						ctx := context.Background()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
						data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
				
			||||||
| 
						 | 
					@ -206,19 +206,20 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlockingTooLarge() {
 | 
				
			||||||
		return io.NopCloser(bytes.NewBuffer(b)), int64(len(b)), nil
 | 
							return io.NopCloser(bytes.NewBuffer(b)), int64(len(b)), nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	emojiID := "01GDQ9G782X42BAMFASKP64343"
 | 
						processing, err := suite.manager.CreateEmoji(ctx,
 | 
				
			||||||
	emojiURI := "http://localhost:8080/emoji/01GDQ9G782X42BAMFASKP64343"
 | 
							"big_panda",
 | 
				
			||||||
 | 
							"",
 | 
				
			||||||
	processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, "big_panda", emojiID, emojiURI, nil, false)
 | 
							data,
 | 
				
			||||||
 | 
							media.AdditionalEmojiInfo{},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// do a blocking call to fetch the emoji
 | 
						// do a blocking call to fetch the emoji
 | 
				
			||||||
	emoji, err := processingEmoji.LoadEmoji(ctx)
 | 
						_, err = processing.Load(ctx)
 | 
				
			||||||
	suite.EqualError(err, "store: given emoji size 630kiB greater than max allowed 50.0kiB")
 | 
						suite.EqualError(err, "store: given emoji size 630kiB greater than max allowed 50.0kiB")
 | 
				
			||||||
	suite.Nil(emoji)
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (suite *ManagerTestSuite) TestEmojiProcessBlockingTooLargeNoSizeGiven() {
 | 
					func (suite *ManagerTestSuite) TestEmojiProcessTooLargeNoSizeGiven() {
 | 
				
			||||||
	ctx := context.Background()
 | 
						ctx := context.Background()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
						data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
				
			||||||
| 
						 | 
					@ -230,19 +231,20 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlockingTooLargeNoSizeGiven() {
 | 
				
			||||||
		return io.NopCloser(bytes.NewBuffer(b)), -1, nil
 | 
							return io.NopCloser(bytes.NewBuffer(b)), -1, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	emojiID := "01GDQ9G782X42BAMFASKP64343"
 | 
						processing, err := suite.manager.CreateEmoji(ctx,
 | 
				
			||||||
	emojiURI := "http://localhost:8080/emoji/01GDQ9G782X42BAMFASKP64343"
 | 
							"big_panda",
 | 
				
			||||||
 | 
							"",
 | 
				
			||||||
	processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, "big_panda", emojiID, emojiURI, nil, false)
 | 
							data,
 | 
				
			||||||
 | 
							media.AdditionalEmojiInfo{},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// do a blocking call to fetch the emoji
 | 
						// do a blocking call to fetch the emoji
 | 
				
			||||||
	emoji, err := processingEmoji.LoadEmoji(ctx)
 | 
						_, err = processing.Load(ctx)
 | 
				
			||||||
	suite.EqualError(err, "store: calculated emoji size 630kiB greater than max allowed 50.0kiB")
 | 
						suite.EqualError(err, "store: written emoji size 630kiB greater than max allowed 50.0kiB")
 | 
				
			||||||
	suite.Nil(emoji)
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (suite *ManagerTestSuite) TestEmojiProcessBlockingNoFileSizeGiven() {
 | 
					func (suite *ManagerTestSuite) TestEmojiProcessNoFileSizeGiven() {
 | 
				
			||||||
	ctx := context.Background()
 | 
						ctx := context.Background()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
						data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
				
			||||||
| 
						 | 
					@ -254,28 +256,27 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlockingNoFileSizeGiven() {
 | 
				
			||||||
		return io.NopCloser(bytes.NewBuffer(b)), -1, nil
 | 
							return io.NopCloser(bytes.NewBuffer(b)), -1, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	emojiID := "01GDQ9G782X42BAMFASKP64343"
 | 
					 | 
				
			||||||
	emojiURI := "http://localhost:8080/emoji/01GDQ9G782X42BAMFASKP64343"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// process the media with no additional info provided
 | 
						// process the media with no additional info provided
 | 
				
			||||||
	processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, "rainbow_test", emojiID, emojiURI, nil, false)
 | 
						processing, err := suite.manager.CreateEmoji(ctx,
 | 
				
			||||||
 | 
							"rainbow_test",
 | 
				
			||||||
 | 
							"",
 | 
				
			||||||
 | 
							data,
 | 
				
			||||||
 | 
							media.AdditionalEmojiInfo{},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// do a blocking call to fetch the emoji
 | 
						// do a blocking call to fetch the emoji
 | 
				
			||||||
	emoji, err := processingEmoji.LoadEmoji(ctx)
 | 
						emoji, err := processing.Load(ctx)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(emoji)
 | 
						suite.NotNil(emoji)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// make sure it's got the stuff set on it that we expect
 | 
					 | 
				
			||||||
	suite.Equal(emojiID, emoji.ID)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// file meta should be correctly derived from the image
 | 
						// file meta should be correctly derived from the image
 | 
				
			||||||
	suite.Equal("image/png", emoji.ImageContentType)
 | 
						suite.Equal("image/png", emoji.ImageContentType)
 | 
				
			||||||
	suite.Equal("image/png", emoji.ImageStaticContentType)
 | 
						suite.Equal("image/png", emoji.ImageStaticContentType)
 | 
				
			||||||
	suite.Equal(36702, emoji.ImageFileSize)
 | 
						suite.Equal(36702, emoji.ImageFileSize)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// now make sure the emoji is in the database
 | 
						// now make sure the emoji is in the database
 | 
				
			||||||
	dbEmoji, err := suite.db.GetEmojiByID(ctx, emojiID)
 | 
						dbEmoji, err := suite.db.GetEmojiByID(ctx, emoji.ID)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(dbEmoji)
 | 
						suite.NotNil(dbEmoji)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -316,27 +317,27 @@ func (suite *ManagerTestSuite) TestEmojiWebpProcess() {
 | 
				
			||||||
		return io.NopCloser(bytes.NewBuffer(b)), int64(len(b)), nil
 | 
							return io.NopCloser(bytes.NewBuffer(b)), int64(len(b)), nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	emojiID := "01GDQ9G782X42BAMFASKP64343"
 | 
						// process the media with no additional info provided
 | 
				
			||||||
	emojiURI := "http://localhost:8080/emoji/01GDQ9G782X42BAMFASKP64343"
 | 
						processing, err := suite.manager.CreateEmoji(ctx,
 | 
				
			||||||
 | 
							"nb-flag",
 | 
				
			||||||
	processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, "nb-flag", emojiID, emojiURI, nil, false)
 | 
							"",
 | 
				
			||||||
 | 
							data,
 | 
				
			||||||
 | 
							media.AdditionalEmojiInfo{},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// do a blocking call to fetch the emoji
 | 
						// do a blocking call to fetch the emoji
 | 
				
			||||||
	emoji, err := processingEmoji.LoadEmoji(ctx)
 | 
						emoji, err := processing.Load(ctx)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(emoji)
 | 
						suite.NotNil(emoji)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// make sure it's got the stuff set on it that we expect
 | 
					 | 
				
			||||||
	suite.Equal(emojiID, emoji.ID)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// file meta should be correctly derived from the image
 | 
						// file meta should be correctly derived from the image
 | 
				
			||||||
	suite.Equal("image/webp", emoji.ImageContentType)
 | 
						suite.Equal("image/webp", emoji.ImageContentType)
 | 
				
			||||||
	suite.Equal("image/png", emoji.ImageStaticContentType)
 | 
						suite.Equal("image/png", emoji.ImageStaticContentType)
 | 
				
			||||||
	suite.Equal(294, emoji.ImageFileSize)
 | 
						suite.Equal(294, emoji.ImageFileSize)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// now make sure the emoji is in the database
 | 
						// now make sure the emoji is in the database
 | 
				
			||||||
	dbEmoji, err := suite.db.GetEmojiByID(ctx, emojiID)
 | 
						dbEmoji, err := suite.db.GetEmojiByID(ctx, emoji.ID)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(dbEmoji)
 | 
						suite.NotNil(dbEmoji)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -365,7 +366,7 @@ func (suite *ManagerTestSuite) TestEmojiWebpProcess() {
 | 
				
			||||||
	suite.Equal(processedStaticBytesExpected, processedStaticBytes)
 | 
						suite.Equal(processedStaticBytesExpected, processedStaticBytes)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (suite *ManagerTestSuite) TestSimpleJpegProcessBlocking() {
 | 
					func (suite *ManagerTestSuite) TestSimpleJpegProcess() {
 | 
				
			||||||
	ctx := context.Background()
 | 
						ctx := context.Background()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
						data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
				
			||||||
| 
						 | 
					@ -380,18 +381,22 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlocking() {
 | 
				
			||||||
	accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
 | 
						accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// process the media with no additional info provided
 | 
						// process the media with no additional info provided
 | 
				
			||||||
	processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
 | 
						processing, err := suite.manager.CreateMedia(ctx,
 | 
				
			||||||
	// fetch the attachment id from the processing media
 | 
							accountID,
 | 
				
			||||||
	attachmentID := processingMedia.AttachmentID()
 | 
							data,
 | 
				
			||||||
 | 
							media.AdditionalMediaInfo{},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
						suite.NotNil(processing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// do a blocking call to fetch the attachment
 | 
						// do a blocking call to fetch the attachment
 | 
				
			||||||
	attachment, err := processingMedia.LoadAttachment(ctx)
 | 
						attachment, err := processing.Load(ctx)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(attachment)
 | 
						suite.NotNil(attachment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// make sure it's got the stuff set on it that we expect
 | 
						// make sure it's got the stuff set on it that we expect
 | 
				
			||||||
	// the attachment ID and accountID we expect
 | 
						// the attachment ID and accountID we expect
 | 
				
			||||||
	suite.Equal(attachmentID, attachment.ID)
 | 
						suite.Equal(processing.ID(), attachment.ID)
 | 
				
			||||||
	suite.Equal(accountID, attachment.AccountID)
 | 
						suite.Equal(accountID, attachment.AccountID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// file meta should be correctly derived from the image
 | 
						// file meta should be correctly derived from the image
 | 
				
			||||||
| 
						 | 
					@ -407,7 +412,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlocking() {
 | 
				
			||||||
	suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
 | 
						suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// now make sure the attachment is in the database
 | 
						// now make sure the attachment is in the database
 | 
				
			||||||
	dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
 | 
						dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(dbAttachment)
 | 
						suite.NotNil(dbAttachment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -456,13 +461,16 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessPartial() {
 | 
				
			||||||
	accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
 | 
						accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// process the media with no additional info provided
 | 
						// process the media with no additional info provided
 | 
				
			||||||
	processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
 | 
						processing, err := suite.manager.CreateMedia(ctx,
 | 
				
			||||||
 | 
							accountID,
 | 
				
			||||||
	// fetch the attachment id from the processing media
 | 
							data,
 | 
				
			||||||
	attachmentID := processingMedia.AttachmentID()
 | 
							media.AdditionalMediaInfo{},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
						suite.NotNil(processing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// do a blocking call to fetch the attachment
 | 
						// do a blocking call to fetch the attachment
 | 
				
			||||||
	attachment, err := processingMedia.LoadAttachment(ctx)
 | 
						attachment, err := processing.Load(ctx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Since we're cutting off the byte stream
 | 
						// Since we're cutting off the byte stream
 | 
				
			||||||
	// halfway through, we should get an error here.
 | 
						// halfway through, we should get an error here.
 | 
				
			||||||
| 
						 | 
					@ -471,17 +479,16 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessPartial() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// make sure it's got the stuff set on it that we expect
 | 
						// make sure it's got the stuff set on it that we expect
 | 
				
			||||||
	// the attachment ID and accountID we expect
 | 
						// the attachment ID and accountID we expect
 | 
				
			||||||
	suite.Equal(attachmentID, attachment.ID)
 | 
						suite.Equal(processing.ID(), attachment.ID)
 | 
				
			||||||
	suite.Equal(accountID, attachment.AccountID)
 | 
						suite.Equal(accountID, attachment.AccountID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// file meta should be correctly derived from the image
 | 
						// file meta should be correctly derived from the image
 | 
				
			||||||
	suite.Zero(attachment.FileMeta)
 | 
						suite.Zero(attachment.FileMeta)
 | 
				
			||||||
	suite.Equal("image/jpeg", attachment.File.ContentType)
 | 
						suite.Equal("image/jpeg", attachment.File.ContentType)
 | 
				
			||||||
	suite.Equal("image/jpeg", attachment.Thumbnail.ContentType)
 | 
					 | 
				
			||||||
	suite.Empty(attachment.Blurhash)
 | 
						suite.Empty(attachment.Blurhash)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// now make sure the attachment is in the database
 | 
						// now make sure the attachment is in the database
 | 
				
			||||||
	dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
 | 
						dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(dbAttachment)
 | 
						suite.NotNil(dbAttachment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -518,19 +525,22 @@ func (suite *ManagerTestSuite) TestPDFProcess() {
 | 
				
			||||||
	accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
 | 
						accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// process the media with no additional info provided
 | 
						// process the media with no additional info provided
 | 
				
			||||||
	processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
 | 
						processing, err := suite.manager.CreateMedia(ctx,
 | 
				
			||||||
 | 
							accountID,
 | 
				
			||||||
	// fetch the attachment id from the processing media
 | 
							data,
 | 
				
			||||||
	attachmentID := processingMedia.AttachmentID()
 | 
							media.AdditionalMediaInfo{},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
						suite.NotNil(processing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// do a blocking call to fetch the attachment
 | 
						// do a blocking call to fetch the attachment
 | 
				
			||||||
	attachment, err := processingMedia.LoadAttachment(ctx)
 | 
						attachment, err := processing.Load(ctx)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(attachment)
 | 
						suite.NotNil(attachment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// make sure it's got the stuff set on it that we expect
 | 
						// make sure it's got the stuff set on it that we expect
 | 
				
			||||||
	// the attachment ID and accountID we expect
 | 
						// the attachment ID and accountID we expect
 | 
				
			||||||
	suite.Equal(attachmentID, attachment.ID)
 | 
						suite.Equal(processing.ID(), attachment.ID)
 | 
				
			||||||
	suite.Equal(accountID, attachment.AccountID)
 | 
						suite.Equal(accountID, attachment.AccountID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// file meta should be correctly derived from the image
 | 
						// file meta should be correctly derived from the image
 | 
				
			||||||
| 
						 | 
					@ -540,7 +550,7 @@ func (suite *ManagerTestSuite) TestPDFProcess() {
 | 
				
			||||||
	suite.Empty(attachment.Blurhash)
 | 
						suite.Empty(attachment.Blurhash)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// now make sure the attachment is in the database
 | 
						// now make sure the attachment is in the database
 | 
				
			||||||
	dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
 | 
						dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(dbAttachment)
 | 
						suite.NotNil(dbAttachment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -561,7 +571,7 @@ func (suite *ManagerTestSuite) TestPDFProcess() {
 | 
				
			||||||
	suite.False(stored)
 | 
						suite.False(stored)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (suite *ManagerTestSuite) TestSlothVineProcessBlocking() {
 | 
					func (suite *ManagerTestSuite) TestSlothVineProcess() {
 | 
				
			||||||
	ctx := context.Background()
 | 
						ctx := context.Background()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
						data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
				
			||||||
| 
						 | 
					@ -576,18 +586,22 @@ func (suite *ManagerTestSuite) TestSlothVineProcessBlocking() {
 | 
				
			||||||
	accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
 | 
						accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// process the media with no additional info provided
 | 
						// process the media with no additional info provided
 | 
				
			||||||
	processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
 | 
						processing, err := suite.manager.CreateMedia(ctx,
 | 
				
			||||||
	// fetch the attachment id from the processing media
 | 
							accountID,
 | 
				
			||||||
	attachmentID := processingMedia.AttachmentID()
 | 
							data,
 | 
				
			||||||
 | 
							media.AdditionalMediaInfo{},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
						suite.NotNil(processing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// do a blocking call to fetch the attachment
 | 
						// do a blocking call to fetch the attachment
 | 
				
			||||||
	attachment, err := processingMedia.LoadAttachment(ctx)
 | 
						attachment, err := processing.Load(ctx)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(attachment)
 | 
						suite.NotNil(attachment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// make sure it's got the stuff set on it that we expect
 | 
						// make sure it's got the stuff set on it that we expect
 | 
				
			||||||
	// the attachment ID and accountID we expect
 | 
						// the attachment ID and accountID we expect
 | 
				
			||||||
	suite.Equal(attachmentID, attachment.ID)
 | 
						suite.Equal(processing.ID(), attachment.ID)
 | 
				
			||||||
	suite.Equal(accountID, attachment.AccountID)
 | 
						suite.Equal(accountID, attachment.AccountID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// file meta should be correctly derived from the video
 | 
						// file meta should be correctly derived from the video
 | 
				
			||||||
| 
						 | 
					@ -607,7 +621,7 @@ func (suite *ManagerTestSuite) TestSlothVineProcessBlocking() {
 | 
				
			||||||
	suite.Equal("L00000fQfQfQfQfQfQfQfQfQfQfQ", attachment.Blurhash)
 | 
						suite.Equal("L00000fQfQfQfQfQfQfQfQfQfQfQ", attachment.Blurhash)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// now make sure the attachment is in the database
 | 
						// now make sure the attachment is in the database
 | 
				
			||||||
	dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
 | 
						dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(dbAttachment)
 | 
						suite.NotNil(dbAttachment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -636,7 +650,7 @@ func (suite *ManagerTestSuite) TestSlothVineProcessBlocking() {
 | 
				
			||||||
	suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
 | 
						suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (suite *ManagerTestSuite) TestLongerMp4ProcessBlocking() {
 | 
					func (suite *ManagerTestSuite) TestLongerMp4Process() {
 | 
				
			||||||
	ctx := context.Background()
 | 
						ctx := context.Background()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
						data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
				
			||||||
| 
						 | 
					@ -651,18 +665,22 @@ func (suite *ManagerTestSuite) TestLongerMp4ProcessBlocking() {
 | 
				
			||||||
	accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
 | 
						accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// process the media with no additional info provided
 | 
						// process the media with no additional info provided
 | 
				
			||||||
	processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
 | 
						processing, err := suite.manager.CreateMedia(ctx,
 | 
				
			||||||
	// fetch the attachment id from the processing media
 | 
							accountID,
 | 
				
			||||||
	attachmentID := processingMedia.AttachmentID()
 | 
							data,
 | 
				
			||||||
 | 
							media.AdditionalMediaInfo{},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
						suite.NotNil(processing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// do a blocking call to fetch the attachment
 | 
						// do a blocking call to fetch the attachment
 | 
				
			||||||
	attachment, err := processingMedia.LoadAttachment(ctx)
 | 
						attachment, err := processing.Load(ctx)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(attachment)
 | 
						suite.NotNil(attachment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// make sure it's got the stuff set on it that we expect
 | 
						// make sure it's got the stuff set on it that we expect
 | 
				
			||||||
	// the attachment ID and accountID we expect
 | 
						// the attachment ID and accountID we expect
 | 
				
			||||||
	suite.Equal(attachmentID, attachment.ID)
 | 
						suite.Equal(processing.ID(), attachment.ID)
 | 
				
			||||||
	suite.Equal(accountID, attachment.AccountID)
 | 
						suite.Equal(accountID, attachment.AccountID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// file meta should be correctly derived from the video
 | 
						// file meta should be correctly derived from the video
 | 
				
			||||||
| 
						 | 
					@ -682,7 +700,7 @@ func (suite *ManagerTestSuite) TestLongerMp4ProcessBlocking() {
 | 
				
			||||||
	suite.Equal("L00000fQfQfQfQfQfQfQfQfQfQfQ", attachment.Blurhash)
 | 
						suite.Equal("L00000fQfQfQfQfQfQfQfQfQfQfQ", attachment.Blurhash)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// now make sure the attachment is in the database
 | 
						// now make sure the attachment is in the database
 | 
				
			||||||
	dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
 | 
						dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(dbAttachment)
 | 
						suite.NotNil(dbAttachment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -711,7 +729,7 @@ func (suite *ManagerTestSuite) TestLongerMp4ProcessBlocking() {
 | 
				
			||||||
	suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
 | 
						suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (suite *ManagerTestSuite) TestBirdnestMp4ProcessBlocking() {
 | 
					func (suite *ManagerTestSuite) TestBirdnestMp4Process() {
 | 
				
			||||||
	ctx := context.Background()
 | 
						ctx := context.Background()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
						data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
				
			||||||
| 
						 | 
					@ -726,18 +744,22 @@ func (suite *ManagerTestSuite) TestBirdnestMp4ProcessBlocking() {
 | 
				
			||||||
	accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
 | 
						accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// process the media with no additional info provided
 | 
						// process the media with no additional info provided
 | 
				
			||||||
	processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
 | 
						processing, err := suite.manager.CreateMedia(ctx,
 | 
				
			||||||
	// fetch the attachment id from the processing media
 | 
							accountID,
 | 
				
			||||||
	attachmentID := processingMedia.AttachmentID()
 | 
							data,
 | 
				
			||||||
 | 
							media.AdditionalMediaInfo{},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
						suite.NotNil(processing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// do a blocking call to fetch the attachment
 | 
						// do a blocking call to fetch the attachment
 | 
				
			||||||
	attachment, err := processingMedia.LoadAttachment(ctx)
 | 
						attachment, err := processing.Load(ctx)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(attachment)
 | 
						suite.NotNil(attachment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// make sure it's got the stuff set on it that we expect
 | 
						// make sure it's got the stuff set on it that we expect
 | 
				
			||||||
	// the attachment ID and accountID we expect
 | 
						// the attachment ID and accountID we expect
 | 
				
			||||||
	suite.Equal(attachmentID, attachment.ID)
 | 
						suite.Equal(processing.ID(), attachment.ID)
 | 
				
			||||||
	suite.Equal(accountID, attachment.AccountID)
 | 
						suite.Equal(accountID, attachment.AccountID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// file meta should be correctly derived from the video
 | 
						// file meta should be correctly derived from the video
 | 
				
			||||||
| 
						 | 
					@ -757,7 +779,7 @@ func (suite *ManagerTestSuite) TestBirdnestMp4ProcessBlocking() {
 | 
				
			||||||
	suite.Equal("L00000fQfQfQfQfQfQfQfQfQfQfQ", attachment.Blurhash)
 | 
						suite.Equal("L00000fQfQfQfQfQfQfQfQfQfQfQ", attachment.Blurhash)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// now make sure the attachment is in the database
 | 
						// now make sure the attachment is in the database
 | 
				
			||||||
	dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
 | 
						dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(dbAttachment)
 | 
						suite.NotNil(dbAttachment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -786,7 +808,7 @@ func (suite *ManagerTestSuite) TestBirdnestMp4ProcessBlocking() {
 | 
				
			||||||
	suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
 | 
						suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (suite *ManagerTestSuite) TestNotAnMp4ProcessBlocking() {
 | 
					func (suite *ManagerTestSuite) TestNotAnMp4Process() {
 | 
				
			||||||
	// try to load an 'mp4' that's actually an mkv in disguise
 | 
						// try to load an 'mp4' that's actually an mkv in disguise
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx := context.Background()
 | 
						ctx := context.Background()
 | 
				
			||||||
| 
						 | 
					@ -803,10 +825,16 @@ func (suite *ManagerTestSuite) TestNotAnMp4ProcessBlocking() {
 | 
				
			||||||
	accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
 | 
						accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// pre processing should go fine but...
 | 
						// pre processing should go fine but...
 | 
				
			||||||
	processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
 | 
						processing, err := suite.manager.CreateMedia(ctx,
 | 
				
			||||||
 | 
							accountID,
 | 
				
			||||||
 | 
							data,
 | 
				
			||||||
 | 
							media.AdditionalMediaInfo{},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
						suite.NotNil(processing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// we should get an error while loading
 | 
						// we should get an error while loading
 | 
				
			||||||
	attachment, err := processingMedia.LoadAttachment(ctx)
 | 
						attachment, err := processing.Load(ctx)
 | 
				
			||||||
	suite.EqualError(err, "finish: error decoding video: error determining video metadata: [width height framerate]")
 | 
						suite.EqualError(err, "finish: error decoding video: error determining video metadata: [width height framerate]")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// partial attachment should be
 | 
						// partial attachment should be
 | 
				
			||||||
| 
						 | 
					@ -815,7 +843,7 @@ func (suite *ManagerTestSuite) TestNotAnMp4ProcessBlocking() {
 | 
				
			||||||
	suite.Equal(gtsmodel.FileTypeUnknown, attachment.Type)
 | 
						suite.Equal(gtsmodel.FileTypeUnknown, attachment.Type)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingNoContentLengthGiven() {
 | 
					func (suite *ManagerTestSuite) TestSimpleJpegProcessNoContentLengthGiven() {
 | 
				
			||||||
	ctx := context.Background()
 | 
						ctx := context.Background()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
						data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
				
			||||||
| 
						 | 
					@ -831,18 +859,22 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingNoContentLengthGiven
 | 
				
			||||||
	accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
 | 
						accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// process the media with no additional info provided
 | 
						// process the media with no additional info provided
 | 
				
			||||||
	processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
 | 
						processing, err := suite.manager.CreateMedia(ctx,
 | 
				
			||||||
	// fetch the attachment id from the processing media
 | 
							accountID,
 | 
				
			||||||
	attachmentID := processingMedia.AttachmentID()
 | 
							data,
 | 
				
			||||||
 | 
							media.AdditionalMediaInfo{},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
						suite.NotNil(processing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// do a blocking call to fetch the attachment
 | 
						// do a blocking call to fetch the attachment
 | 
				
			||||||
	attachment, err := processingMedia.LoadAttachment(ctx)
 | 
						attachment, err := processing.Load(ctx)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(attachment)
 | 
						suite.NotNil(attachment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// make sure it's got the stuff set on it that we expect
 | 
						// make sure it's got the stuff set on it that we expect
 | 
				
			||||||
	// the attachment ID and accountID we expect
 | 
						// the attachment ID and accountID we expect
 | 
				
			||||||
	suite.Equal(attachmentID, attachment.ID)
 | 
						suite.Equal(processing.ID(), attachment.ID)
 | 
				
			||||||
	suite.Equal(accountID, attachment.AccountID)
 | 
						suite.Equal(accountID, attachment.AccountID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// file meta should be correctly derived from the image
 | 
						// file meta should be correctly derived from the image
 | 
				
			||||||
| 
						 | 
					@ -858,7 +890,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingNoContentLengthGiven
 | 
				
			||||||
	suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
 | 
						suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// now make sure the attachment is in the database
 | 
						// now make sure the attachment is in the database
 | 
				
			||||||
	dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
 | 
						dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(dbAttachment)
 | 
						suite.NotNil(dbAttachment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -887,7 +919,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingNoContentLengthGiven
 | 
				
			||||||
	suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
 | 
						suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingReadCloser() {
 | 
					func (suite *ManagerTestSuite) TestSimpleJpegProcessReadCloser() {
 | 
				
			||||||
	ctx := context.Background()
 | 
						ctx := context.Background()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
						data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
				
			||||||
| 
						 | 
					@ -903,18 +935,22 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingReadCloser() {
 | 
				
			||||||
	accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
 | 
						accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// process the media with no additional info provided
 | 
						// process the media with no additional info provided
 | 
				
			||||||
	processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
 | 
						processing, err := suite.manager.CreateMedia(ctx,
 | 
				
			||||||
	// fetch the attachment id from the processing media
 | 
							accountID,
 | 
				
			||||||
	attachmentID := processingMedia.AttachmentID()
 | 
							data,
 | 
				
			||||||
 | 
							media.AdditionalMediaInfo{},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
						suite.NotNil(processing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// do a blocking call to fetch the attachment
 | 
						// do a blocking call to fetch the attachment
 | 
				
			||||||
	attachment, err := processingMedia.LoadAttachment(ctx)
 | 
						attachment, err := processing.Load(ctx)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(attachment)
 | 
						suite.NotNil(attachment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// make sure it's got the stuff set on it that we expect
 | 
						// make sure it's got the stuff set on it that we expect
 | 
				
			||||||
	// the attachment ID and accountID we expect
 | 
						// the attachment ID and accountID we expect
 | 
				
			||||||
	suite.Equal(attachmentID, attachment.ID)
 | 
						suite.Equal(processing.ID(), attachment.ID)
 | 
				
			||||||
	suite.Equal(accountID, attachment.AccountID)
 | 
						suite.Equal(accountID, attachment.AccountID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// file meta should be correctly derived from the image
 | 
						// file meta should be correctly derived from the image
 | 
				
			||||||
| 
						 | 
					@ -930,7 +966,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingReadCloser() {
 | 
				
			||||||
	suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
 | 
						suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// now make sure the attachment is in the database
 | 
						// now make sure the attachment is in the database
 | 
				
			||||||
	dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
 | 
						dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(dbAttachment)
 | 
						suite.NotNil(dbAttachment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -959,7 +995,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingReadCloser() {
 | 
				
			||||||
	suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
 | 
						suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (suite *ManagerTestSuite) TestPngNoAlphaChannelProcessBlocking() {
 | 
					func (suite *ManagerTestSuite) TestPngNoAlphaChannelProcess() {
 | 
				
			||||||
	ctx := context.Background()
 | 
						ctx := context.Background()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
						data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
				
			||||||
| 
						 | 
					@ -974,18 +1010,22 @@ func (suite *ManagerTestSuite) TestPngNoAlphaChannelProcessBlocking() {
 | 
				
			||||||
	accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
 | 
						accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// process the media with no additional info provided
 | 
						// process the media with no additional info provided
 | 
				
			||||||
	processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
 | 
						processing, err := suite.manager.CreateMedia(ctx,
 | 
				
			||||||
	// fetch the attachment id from the processing media
 | 
							accountID,
 | 
				
			||||||
	attachmentID := processingMedia.AttachmentID()
 | 
							data,
 | 
				
			||||||
 | 
							media.AdditionalMediaInfo{},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
						suite.NotNil(processing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// do a blocking call to fetch the attachment
 | 
						// do a blocking call to fetch the attachment
 | 
				
			||||||
	attachment, err := processingMedia.LoadAttachment(ctx)
 | 
						attachment, err := processing.Load(ctx)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(attachment)
 | 
						suite.NotNil(attachment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// make sure it's got the stuff set on it that we expect
 | 
						// make sure it's got the stuff set on it that we expect
 | 
				
			||||||
	// the attachment ID and accountID we expect
 | 
						// the attachment ID and accountID we expect
 | 
				
			||||||
	suite.Equal(attachmentID, attachment.ID)
 | 
						suite.Equal(processing.ID(), attachment.ID)
 | 
				
			||||||
	suite.Equal(accountID, attachment.AccountID)
 | 
						suite.Equal(accountID, attachment.AccountID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// file meta should be correctly derived from the image
 | 
						// file meta should be correctly derived from the image
 | 
				
			||||||
| 
						 | 
					@ -1001,7 +1041,7 @@ func (suite *ManagerTestSuite) TestPngNoAlphaChannelProcessBlocking() {
 | 
				
			||||||
	suite.Equal("LFQT7e.A%O%4?co$M}M{_1W9~TxV", attachment.Blurhash)
 | 
						suite.Equal("LFQT7e.A%O%4?co$M}M{_1W9~TxV", attachment.Blurhash)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// now make sure the attachment is in the database
 | 
						// now make sure the attachment is in the database
 | 
				
			||||||
	dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
 | 
						dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(dbAttachment)
 | 
						suite.NotNil(dbAttachment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1030,7 +1070,7 @@ func (suite *ManagerTestSuite) TestPngNoAlphaChannelProcessBlocking() {
 | 
				
			||||||
	suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
 | 
						suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (suite *ManagerTestSuite) TestPngAlphaChannelProcessBlocking() {
 | 
					func (suite *ManagerTestSuite) TestPngAlphaChannelProcess() {
 | 
				
			||||||
	ctx := context.Background()
 | 
						ctx := context.Background()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
						data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
				
			||||||
| 
						 | 
					@ -1045,18 +1085,22 @@ func (suite *ManagerTestSuite) TestPngAlphaChannelProcessBlocking() {
 | 
				
			||||||
	accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
 | 
						accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// process the media with no additional info provided
 | 
						// process the media with no additional info provided
 | 
				
			||||||
	processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
 | 
						processing, err := suite.manager.CreateMedia(ctx,
 | 
				
			||||||
	// fetch the attachment id from the processing media
 | 
							accountID,
 | 
				
			||||||
	attachmentID := processingMedia.AttachmentID()
 | 
							data,
 | 
				
			||||||
 | 
							media.AdditionalMediaInfo{},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
						suite.NotNil(processing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// do a blocking call to fetch the attachment
 | 
						// do a blocking call to fetch the attachment
 | 
				
			||||||
	attachment, err := processingMedia.LoadAttachment(ctx)
 | 
						attachment, err := processing.Load(ctx)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(attachment)
 | 
						suite.NotNil(attachment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// make sure it's got the stuff set on it that we expect
 | 
						// make sure it's got the stuff set on it that we expect
 | 
				
			||||||
	// the attachment ID and accountID we expect
 | 
						// the attachment ID and accountID we expect
 | 
				
			||||||
	suite.Equal(attachmentID, attachment.ID)
 | 
						suite.Equal(processing.ID(), attachment.ID)
 | 
				
			||||||
	suite.Equal(accountID, attachment.AccountID)
 | 
						suite.Equal(accountID, attachment.AccountID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// file meta should be correctly derived from the image
 | 
						// file meta should be correctly derived from the image
 | 
				
			||||||
| 
						 | 
					@ -1072,7 +1116,7 @@ func (suite *ManagerTestSuite) TestPngAlphaChannelProcessBlocking() {
 | 
				
			||||||
	suite.Equal("LFQT7e.A%O%4?co$M}M{_1W9~TxV", attachment.Blurhash)
 | 
						suite.Equal("LFQT7e.A%O%4?co$M}M{_1W9~TxV", attachment.Blurhash)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// now make sure the attachment is in the database
 | 
						// now make sure the attachment is in the database
 | 
				
			||||||
	dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
 | 
						dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(dbAttachment)
 | 
						suite.NotNil(dbAttachment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1101,7 +1145,7 @@ func (suite *ManagerTestSuite) TestPngAlphaChannelProcessBlocking() {
 | 
				
			||||||
	suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
 | 
						suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingWithCallback() {
 | 
					func (suite *ManagerTestSuite) TestSimpleJpegProcessWithCallback() {
 | 
				
			||||||
	ctx := context.Background()
 | 
						ctx := context.Background()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
						data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
				
			||||||
| 
						 | 
					@ -1116,18 +1160,22 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingWithCallback() {
 | 
				
			||||||
	accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
 | 
						accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// process the media with no additional info provided
 | 
						// process the media with no additional info provided
 | 
				
			||||||
	processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
 | 
						processing, err := suite.manager.CreateMedia(ctx,
 | 
				
			||||||
	// fetch the attachment id from the processing media
 | 
							accountID,
 | 
				
			||||||
	attachmentID := processingMedia.AttachmentID()
 | 
							data,
 | 
				
			||||||
 | 
							media.AdditionalMediaInfo{},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
						suite.NotNil(processing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// do a blocking call to fetch the attachment
 | 
						// do a blocking call to fetch the attachment
 | 
				
			||||||
	attachment, err := processingMedia.LoadAttachment(ctx)
 | 
						attachment, err := processing.Load(ctx)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(attachment)
 | 
						suite.NotNil(attachment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// make sure it's got the stuff set on it that we expect
 | 
						// make sure it's got the stuff set on it that we expect
 | 
				
			||||||
	// the attachment ID and accountID we expect
 | 
						// the attachment ID and accountID we expect
 | 
				
			||||||
	suite.Equal(attachmentID, attachment.ID)
 | 
						suite.Equal(processing.ID(), attachment.ID)
 | 
				
			||||||
	suite.Equal(accountID, attachment.AccountID)
 | 
						suite.Equal(accountID, attachment.AccountID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// file meta should be correctly derived from the image
 | 
						// file meta should be correctly derived from the image
 | 
				
			||||||
| 
						 | 
					@ -1143,7 +1191,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingWithCallback() {
 | 
				
			||||||
	suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
 | 
						suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// now make sure the attachment is in the database
 | 
						// now make sure the attachment is in the database
 | 
				
			||||||
	dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
 | 
						dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(dbAttachment)
 | 
						suite.NotNil(dbAttachment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1172,7 +1220,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingWithCallback() {
 | 
				
			||||||
	suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
 | 
						suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingWithDiskStorage() {
 | 
					func (suite *ManagerTestSuite) TestSimpleJpegProcessWithDiskStorage() {
 | 
				
			||||||
	ctx := context.Background()
 | 
						ctx := context.Background()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
						data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
				
			||||||
| 
						 | 
					@ -1209,18 +1257,22 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingWithDiskStorage() {
 | 
				
			||||||
	suite.manager = diskManager
 | 
						suite.manager = diskManager
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// process the media with no additional info provided
 | 
						// process the media with no additional info provided
 | 
				
			||||||
	processingMedia := diskManager.PreProcessMedia(data, accountID, nil)
 | 
						processing, err := suite.manager.CreateMedia(ctx,
 | 
				
			||||||
	// fetch the attachment id from the processing media
 | 
							accountID,
 | 
				
			||||||
	attachmentID := processingMedia.AttachmentID()
 | 
							data,
 | 
				
			||||||
 | 
							media.AdditionalMediaInfo{},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
						suite.NotNil(processing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// do a blocking call to fetch the attachment
 | 
						// do a blocking call to fetch the attachment
 | 
				
			||||||
	attachment, err := processingMedia.LoadAttachment(ctx)
 | 
						attachment, err := processing.Load(ctx)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(attachment)
 | 
						suite.NotNil(attachment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// make sure it's got the stuff set on it that we expect
 | 
						// make sure it's got the stuff set on it that we expect
 | 
				
			||||||
	// the attachment ID and accountID we expect
 | 
						// the attachment ID and accountID we expect
 | 
				
			||||||
	suite.Equal(attachmentID, attachment.ID)
 | 
						suite.Equal(processing.ID(), attachment.ID)
 | 
				
			||||||
	suite.Equal(accountID, attachment.AccountID)
 | 
						suite.Equal(accountID, attachment.AccountID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// file meta should be correctly derived from the image
 | 
						// file meta should be correctly derived from the image
 | 
				
			||||||
| 
						 | 
					@ -1236,7 +1288,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingWithDiskStorage() {
 | 
				
			||||||
	suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
 | 
						suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// now make sure the attachment is in the database
 | 
						// now make sure the attachment is in the database
 | 
				
			||||||
	dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
 | 
						dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
 | 
				
			||||||
	suite.NoError(err)
 | 
						suite.NoError(err)
 | 
				
			||||||
	suite.NotNil(dbAttachment)
 | 
						suite.NotNil(dbAttachment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1307,22 +1359,27 @@ func (suite *ManagerTestSuite) TestSmallSizedMediaTypeDetection_issue2263() {
 | 
				
			||||||
			accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
 | 
								accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// process the media with no additional info provided
 | 
								// process the media with no additional info provided
 | 
				
			||||||
			processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
 | 
								processing, err := suite.manager.CreateMedia(ctx,
 | 
				
			||||||
			if _, err := processingMedia.LoadAttachment(ctx); err != nil {
 | 
									accountID,
 | 
				
			||||||
				suite.FailNow(err.Error())
 | 
									data,
 | 
				
			||||||
			}
 | 
									media.AdditionalMediaInfo{},
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
								suite.NoError(err)
 | 
				
			||||||
 | 
								suite.NotNil(processing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			attachmentID := processingMedia.AttachmentID()
 | 
								// Load the attachment (but ignore return).
 | 
				
			||||||
 | 
								_, err = processing.Load(ctx)
 | 
				
			||||||
 | 
								suite.NoError(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// fetch the attachment id from the processing media
 | 
								// fetch the attachment id from the processing media
 | 
				
			||||||
			attachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
 | 
								attachment, err := suite.db.GetAttachmentByID(ctx, processing.ID())
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				suite.FailNow(err.Error())
 | 
									suite.FailNow(err.Error())
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// make sure it's got the stuff set on it that we expect
 | 
								// make sure it's got the stuff set on it that we expect
 | 
				
			||||||
			// the attachment ID and accountID we expect
 | 
								// the attachment ID and accountID we expect
 | 
				
			||||||
			suite.Equal(attachmentID, attachment.ID)
 | 
								suite.Equal(processing.ID(), attachment.ID)
 | 
				
			||||||
			suite.Equal(accountID, attachment.AccountID)
 | 
								suite.Equal(accountID, attachment.AccountID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			actual := attachment.File.ContentType
 | 
								actual := attachment.File.ContentType
 | 
				
			||||||
| 
						 | 
					@ -1350,13 +1407,21 @@ func (suite *ManagerTestSuite) TestMisreportedSmallMedia() {
 | 
				
			||||||
		return io.NopCloser(bytes.NewBuffer(b)), int64(2 * actualSize), nil
 | 
							return io.NopCloser(bytes.NewBuffer(b)), int64(2 * actualSize), nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Process the media with no additional info provided.
 | 
						ctx := context.Background()
 | 
				
			||||||
	attachment, err := suite.manager.
 | 
					
 | 
				
			||||||
		PreProcessMedia(data, accountID, nil).
 | 
						// process the media with no additional info provided
 | 
				
			||||||
		LoadAttachment(context.Background())
 | 
						processing, err := suite.manager.CreateMedia(ctx,
 | 
				
			||||||
	if err != nil {
 | 
							accountID,
 | 
				
			||||||
		suite.FailNow(err.Error())
 | 
							data,
 | 
				
			||||||
	}
 | 
							media.AdditionalMediaInfo{},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
						suite.NotNil(processing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// do a blocking call to fetch the attachment
 | 
				
			||||||
 | 
						attachment, err := processing.Load(ctx)
 | 
				
			||||||
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
						suite.NotNil(attachment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	suite.Equal(actualSize, attachment.File.FileSize)
 | 
						suite.Equal(actualSize, attachment.File.FileSize)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1378,13 +1443,21 @@ func (suite *ManagerTestSuite) TestNoReportedSizeSmallMedia() {
 | 
				
			||||||
		return io.NopCloser(bytes.NewBuffer(b)), 0, nil
 | 
							return io.NopCloser(bytes.NewBuffer(b)), 0, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Process the media with no additional info provided.
 | 
						ctx := context.Background()
 | 
				
			||||||
	attachment, err := suite.manager.
 | 
					
 | 
				
			||||||
		PreProcessMedia(data, accountID, nil).
 | 
						// process the media with no additional info provided
 | 
				
			||||||
		LoadAttachment(context.Background())
 | 
						processing, err := suite.manager.CreateMedia(ctx,
 | 
				
			||||||
	if err != nil {
 | 
							accountID,
 | 
				
			||||||
		suite.FailNow(err.Error())
 | 
							data,
 | 
				
			||||||
	}
 | 
							media.AdditionalMediaInfo{},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
						suite.NotNil(processing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// do a blocking call to fetch the attachment
 | 
				
			||||||
 | 
						attachment, err := processing.Load(ctx)
 | 
				
			||||||
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
						suite.NotNil(attachment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	suite.Equal(actualSize, attachment.File.FileSize)
 | 
						suite.Equal(actualSize, attachment.File.FileSize)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,14 +24,16 @@ import (
 | 
				
			||||||
	"slices"
 | 
						"slices"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"codeberg.org/gruf/go-bytesize"
 | 
						"codeberg.org/gruf/go-bytesize"
 | 
				
			||||||
	"codeberg.org/gruf/go-errors/v2"
 | 
						errorsv2 "codeberg.org/gruf/go-errors/v2"
 | 
				
			||||||
	"codeberg.org/gruf/go-runners"
 | 
						"codeberg.org/gruf/go-runners"
 | 
				
			||||||
	"github.com/h2non/filetype"
 | 
						"github.com/h2non/filetype"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/config"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/config"
 | 
				
			||||||
 | 
						"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/gtserror"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/log"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/log"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/regexes"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/regexes"
 | 
				
			||||||
 | 
						"github.com/superseriousbusiness/gotosocial/internal/storage"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/uris"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/uris"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/util"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/util"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
| 
						 | 
					@ -40,7 +42,6 @@ import (
 | 
				
			||||||
// various functions for retrieving data from the process.
 | 
					// various functions for retrieving data from the process.
 | 
				
			||||||
type ProcessingEmoji struct {
 | 
					type ProcessingEmoji struct {
 | 
				
			||||||
	emoji     *gtsmodel.Emoji   // processing emoji details
 | 
						emoji     *gtsmodel.Emoji   // processing emoji details
 | 
				
			||||||
	existing  bool              // indicates whether this is an existing emoji ID being refreshed / recached
 | 
					 | 
				
			||||||
	newPathID string            // new emoji path ID to use when being refreshed
 | 
						newPathID string            // new emoji path ID to use when being refreshed
 | 
				
			||||||
	dataFn    DataFunc          // load-data function, returns media stream
 | 
						dataFn    DataFunc          // load-data function, returns media stream
 | 
				
			||||||
	done      bool              // done is set when process finishes with non ctx canceled type error
 | 
						done      bool              // done is set when process finishes with non ctx canceled type error
 | 
				
			||||||
| 
						 | 
					@ -49,61 +50,72 @@ type ProcessingEmoji struct {
 | 
				
			||||||
	mgr       *Manager          // mgr instance (access to db / storage)
 | 
						mgr       *Manager          // mgr instance (access to db / storage)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// EmojiID returns the ID of the underlying emoji without blocking processing.
 | 
					// ID returns the ID of the underlying emoji.
 | 
				
			||||||
func (p *ProcessingEmoji) EmojiID() string {
 | 
					func (p *ProcessingEmoji) ID() string {
 | 
				
			||||||
	return p.emoji.ID // immutable, safe outside mutex.
 | 
						return p.emoji.ID // immutable, safe outside mutex.
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// LoadEmoji blocks until the static and fullsize image has been processed, and then returns the completed emoji.
 | 
					// LoadEmoji blocks until the static and fullsize image has been processed, and then returns the completed emoji.
 | 
				
			||||||
func (p *ProcessingEmoji) LoadEmoji(ctx context.Context) (*gtsmodel.Emoji, error) {
 | 
					func (p *ProcessingEmoji) Load(ctx context.Context) (*gtsmodel.Emoji, error) {
 | 
				
			||||||
	// Attempt to load synchronously.
 | 
					 | 
				
			||||||
	emoji, done, err := p.load(ctx)
 | 
						emoji, done, err := p.load(ctx)
 | 
				
			||||||
	if err == nil {
 | 
					 | 
				
			||||||
		// No issue, return media.
 | 
					 | 
				
			||||||
		return emoji, nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if !done {
 | 
						if !done {
 | 
				
			||||||
		// Provided context was cancelled, e.g. request cancelled
 | 
							// On a context-canceled error (marked as !done), requeue for loading.
 | 
				
			||||||
		// early. Queue this item for asynchronous processing.
 | 
							p.mgr.state.Workers.Dereference.Queue.Push(func(ctx context.Context) {
 | 
				
			||||||
		log.Warnf(ctx, "reprocessing emoji %s after canceled ctx", p.emoji.ID)
 | 
					 | 
				
			||||||
		p.mgr.state.Workers.Media.Queue.Push(p.Process)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return nil, err
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Process allows the receiving object to fit the runners.WorkerFunc signature. It performs a (blocking) load and logs on error.
 | 
					 | 
				
			||||||
func (p *ProcessingEmoji) Process(ctx context.Context) {
 | 
					 | 
				
			||||||
			if _, _, err := p.load(ctx); err != nil {
 | 
								if _, _, err := p.load(ctx); err != nil {
 | 
				
			||||||
		log.Errorf(ctx, "error processing emoji: %v", err)
 | 
									log.Errorf(ctx, "error loading emoji: %v", err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return emoji, err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// load performs a concurrency-safe load of ProcessingEmoji, only marking itself as complete when returned error is NOT a context cancel.
 | 
					// load is the package private form of load() that is wrapped to catch context canceled.
 | 
				
			||||||
func (p *ProcessingEmoji) load(ctx context.Context) (*gtsmodel.Emoji, bool, error) {
 | 
					func (p *ProcessingEmoji) load(ctx context.Context) (
 | 
				
			||||||
	var (
 | 
						emoji *gtsmodel.Emoji,
 | 
				
			||||||
		done bool
 | 
						done bool,
 | 
				
			||||||
		err  error
 | 
						err error,
 | 
				
			||||||
	)
 | 
					) {
 | 
				
			||||||
 | 
					 | 
				
			||||||
	err = p.proc.Process(func() error {
 | 
						err = p.proc.Process(func() error {
 | 
				
			||||||
		if p.done {
 | 
							if done = p.done; done {
 | 
				
			||||||
			// Already proc'd.
 | 
								// Already proc'd.
 | 
				
			||||||
			return p.err
 | 
								return p.err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		defer func() {
 | 
							defer func() {
 | 
				
			||||||
			// This is only done when ctx NOT cancelled.
 | 
								// This is only done when ctx NOT cancelled.
 | 
				
			||||||
			done = err == nil || !errors.IsV2(err,
 | 
								done = (err == nil || !errorsv2.IsV2(err,
 | 
				
			||||||
				context.Canceled,
 | 
									context.Canceled,
 | 
				
			||||||
				context.DeadlineExceeded,
 | 
									context.DeadlineExceeded,
 | 
				
			||||||
			)
 | 
								))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if !done {
 | 
								if !done {
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Anything from here, we
 | 
				
			||||||
 | 
								// need to ensure happens
 | 
				
			||||||
 | 
								// (i.e. no ctx canceled).
 | 
				
			||||||
 | 
								ctx = gtscontext.WithValues(
 | 
				
			||||||
 | 
									context.Background(),
 | 
				
			||||||
 | 
									ctx, // values
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// On error, clean
 | 
				
			||||||
 | 
								// downloaded files.
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									p.cleanup(ctx)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if !done {
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Update with latest details, whatever happened.
 | 
				
			||||||
 | 
								e := p.mgr.state.DB.UpdateEmoji(ctx, p.emoji)
 | 
				
			||||||
 | 
								if e != nil {
 | 
				
			||||||
 | 
									log.Errorf(ctx, "error updating emoji in db: %v", e)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// Store final values.
 | 
								// Store final values.
 | 
				
			||||||
			p.done = true
 | 
								p.done = true
 | 
				
			||||||
			p.err = err
 | 
								p.err = err
 | 
				
			||||||
| 
						 | 
					@ -111,39 +123,31 @@ func (p *ProcessingEmoji) load(ctx context.Context) (*gtsmodel.Emoji, bool, erro
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Attempt to store media and calculate
 | 
							// Attempt to store media and calculate
 | 
				
			||||||
		// full-size media attachment details.
 | 
							// full-size media attachment details.
 | 
				
			||||||
 | 
							//
 | 
				
			||||||
 | 
							// This will update p.emoji as it goes.
 | 
				
			||||||
		if err = p.store(ctx); err != nil {
 | 
							if err = p.store(ctx); err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Finish processing by reloading media into
 | 
							// Finish processing by reloading media into
 | 
				
			||||||
		// memory to get dimension and generate a thumb.
 | 
							// memory to get dimension and generate a thumb.
 | 
				
			||||||
 | 
							//
 | 
				
			||||||
 | 
							// This will update p.emoji as it goes.
 | 
				
			||||||
		if err = p.finish(ctx); err != nil {
 | 
							if err = p.finish(ctx); err != nil {
 | 
				
			||||||
			return err
 | 
								return err //nolint:revive
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if p.existing {
 | 
							return nil
 | 
				
			||||||
			// Existing emoji we're updating, so only update.
 | 
					 | 
				
			||||||
			err = p.mgr.state.DB.UpdateEmoji(ctx, p.emoji)
 | 
					 | 
				
			||||||
			return err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// New emoji media, first time caching.
 | 
					 | 
				
			||||||
		err = p.mgr.state.DB.PutEmoji(ctx, p.emoji)
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
						emoji = p.emoji
 | 
				
			||||||
	if err != nil {
 | 
						return
 | 
				
			||||||
		return nil, done, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return p.emoji, done, nil
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// store calls the data function attached to p if it hasn't been called yet,
 | 
					// store calls the data function attached to p if it hasn't been called yet,
 | 
				
			||||||
// and updates the underlying attachment fields as necessary. It will then stream
 | 
					// and updates the underlying attachment fields as necessary. It will then stream
 | 
				
			||||||
// bytes from p's reader directly into storage so that it can be retrieved later.
 | 
					// bytes from p's reader directly into storage so that it can be retrieved later.
 | 
				
			||||||
func (p *ProcessingEmoji) store(ctx context.Context) error {
 | 
					func (p *ProcessingEmoji) store(ctx context.Context) error {
 | 
				
			||||||
	// Load media from provided data fn.
 | 
						// Load media from provided data fun
 | 
				
			||||||
	rc, sz, err := p.dataFn(ctx)
 | 
						rc, sz, err := p.dataFn(ctx)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return gtserror.Newf("error executing data function: %w", err)
 | 
							return gtserror.Newf("error executing data function: %w", err)
 | 
				
			||||||
| 
						 | 
					@ -168,8 +172,9 @@ func (p *ProcessingEmoji) store(ctx context.Context) error {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Check that provided size isn't beyond max. We check beforehand
 | 
						// Check that provided size isn't beyond max. We check beforehand
 | 
				
			||||||
	// so that we don't attempt to stream the emoji into storage if not needed.
 | 
						// so that we don't attempt to stream the emoji into storage if not needed.
 | 
				
			||||||
	if size := bytesize.Size(sz); sz > 0 && size > maxSize {
 | 
						if sz > 0 && sz > int64(maxSize) {
 | 
				
			||||||
		return gtserror.Newf("given emoji size %s greater than max allowed %s", size, maxSize)
 | 
							sz := bytesize.Size(sz) // improves log readability
 | 
				
			||||||
 | 
							return gtserror.Newf("given emoji size %s greater than max allowed %s", sz, maxSize)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Prepare to read bytes from
 | 
						// Prepare to read bytes from
 | 
				
			||||||
| 
						 | 
					@ -196,14 +201,14 @@ func (p *ProcessingEmoji) store(ctx context.Context) error {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Initial file size was misreported, so we didn't read
 | 
							// Initial file size was misreported, so we didn't read
 | 
				
			||||||
		// fully into hdrBuf. Reslice it to the size we did read.
 | 
							// fully into hdrBuf. Reslice it to the size we did read.
 | 
				
			||||||
		log.Warnf(ctx,
 | 
					 | 
				
			||||||
			"recovered from misreported file size; reported %d; read %d",
 | 
					 | 
				
			||||||
			fileSize, n,
 | 
					 | 
				
			||||||
		)
 | 
					 | 
				
			||||||
		hdrBuf = hdrBuf[:n]
 | 
							hdrBuf = hdrBuf[:n]
 | 
				
			||||||
 | 
							fileSize = n
 | 
				
			||||||
 | 
							p.emoji.ImageFileSize = fileSize
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Parse file type info from header buffer.
 | 
						// Parse file type info from header buffer.
 | 
				
			||||||
 | 
						// This should only ever error if the buffer
 | 
				
			||||||
 | 
						// is empty (ie., the attachment is 0 bytes).
 | 
				
			||||||
	info, err := filetype.Match(hdrBuf)
 | 
						info, err := filetype.Match(hdrBuf)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return gtserror.Newf("error parsing file type: %w", err)
 | 
							return gtserror.Newf("error parsing file type: %w", err)
 | 
				
			||||||
| 
						 | 
					@ -227,10 +232,13 @@ func (p *ProcessingEmoji) store(ctx context.Context) error {
 | 
				
			||||||
		pathID = p.emoji.ID
 | 
							pathID = p.emoji.ID
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Determine instance account ID from already generated image static path.
 | 
						// Determine instance account ID from generated image static path.
 | 
				
			||||||
	instanceAccID := regexes.FilePath.FindStringSubmatch(p.emoji.ImageStaticPath)[1]
 | 
						instanceAccID, ok := getInstanceAccountID(p.emoji.ImageStaticPath)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return gtserror.Newf("invalid emoji static path; no instance account id: %s", p.emoji.ImageStaticPath)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Calculate emoji file path.
 | 
						// Calculate final media attachment file path.
 | 
				
			||||||
	p.emoji.ImagePath = uris.StoragePathForAttachment(
 | 
						p.emoji.ImagePath = uris.StoragePathForAttachment(
 | 
				
			||||||
		instanceAccID,
 | 
							instanceAccID,
 | 
				
			||||||
		string(TypeEmoji),
 | 
							string(TypeEmoji),
 | 
				
			||||||
| 
						 | 
					@ -239,32 +247,32 @@ func (p *ProcessingEmoji) store(ctx context.Context) error {
 | 
				
			||||||
		info.Extension,
 | 
							info.Extension,
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// This shouldn't already exist, but we do a check as it's worth logging.
 | 
						// File shouldn't already exist in storage at this point,
 | 
				
			||||||
 | 
						// but we do a check as it's worth logging / cleaning up.
 | 
				
			||||||
	if have, _ := p.mgr.state.Storage.Has(ctx, p.emoji.ImagePath); have {
 | 
						if have, _ := p.mgr.state.Storage.Has(ctx, p.emoji.ImagePath); have {
 | 
				
			||||||
		log.Warnf(ctx, "emoji already exists at storage path: %s", p.emoji.ImagePath)
 | 
							log.Warnf(ctx, "emoji already exists at: %s", p.emoji.ImagePath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Attempt to remove existing emoji at storage path (might be broken / out-of-date)
 | 
							// Attempt to remove existing emoji at storage path (might be broken / out-of-date)
 | 
				
			||||||
		if err := p.mgr.state.Storage.Delete(ctx, p.emoji.ImagePath); err != nil {
 | 
							if err := p.mgr.state.Storage.Delete(ctx, p.emoji.ImagePath); err != nil {
 | 
				
			||||||
			return gtserror.Newf("error removing emoji from storage: %v", err)
 | 
								return gtserror.Newf("error removing emoji %s from storage: %v", p.emoji.ImagePath, err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Write the final image reader stream to our storage.
 | 
						// Write the final image reader stream to our storage.
 | 
				
			||||||
	wroteSize, err := p.mgr.state.Storage.PutStream(ctx, p.emoji.ImagePath, r)
 | 
						sz, err = p.mgr.state.Storage.PutStream(ctx, p.emoji.ImagePath, r)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return gtserror.Newf("error writing emoji to storage: %w", err)
 | 
							return gtserror.Newf("error writing emoji to storage: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Once again check size in case none was provided previously.
 | 
						// Perform final size check in case none was
 | 
				
			||||||
	if size := bytesize.Size(wroteSize); size > maxSize {
 | 
						// given previously, or size was mis-reported.
 | 
				
			||||||
		if err := p.mgr.state.Storage.Delete(ctx, p.emoji.ImagePath); err != nil {
 | 
						// (error here will later perform p.cleanup()).
 | 
				
			||||||
			log.Errorf(ctx, "error removing too-large-emoji from storage: %v", err)
 | 
						if sz > int64(maxSize) {
 | 
				
			||||||
 | 
							sz := bytesize.Size(sz) // improves log readability
 | 
				
			||||||
 | 
							return gtserror.Newf("written emoji size %s greater than max allowed %s", sz, maxSize)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return gtserror.Newf("calculated emoji size %s greater than max allowed %s", size, maxSize)
 | 
						// Fill in remaining emoji data now it's stored.
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Fill in remaining attachment data now it's stored.
 | 
					 | 
				
			||||||
	p.emoji.ImageURL = uris.URIForAttachment(
 | 
						p.emoji.ImageURL = uris.URIForAttachment(
 | 
				
			||||||
		instanceAccID,
 | 
							instanceAccID,
 | 
				
			||||||
		string(TypeEmoji),
 | 
							string(TypeEmoji),
 | 
				
			||||||
| 
						 | 
					@ -273,14 +281,14 @@ func (p *ProcessingEmoji) store(ctx context.Context) error {
 | 
				
			||||||
		info.Extension,
 | 
							info.Extension,
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	p.emoji.ImageContentType = info.MIME.Value
 | 
						p.emoji.ImageContentType = info.MIME.Value
 | 
				
			||||||
	p.emoji.ImageFileSize = int(wroteSize)
 | 
						p.emoji.ImageFileSize = int(sz)
 | 
				
			||||||
	p.emoji.Cached = util.Ptr(true)
 | 
						p.emoji.Cached = util.Ptr(true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (p *ProcessingEmoji) finish(ctx context.Context) error {
 | 
					func (p *ProcessingEmoji) finish(ctx context.Context) error {
 | 
				
			||||||
	// Fetch a stream to the original file in storage.
 | 
						// Get a stream to the original file for further processing.
 | 
				
			||||||
	rc, err := p.mgr.state.Storage.GetStream(ctx, p.emoji.ImagePath)
 | 
						rc, err := p.mgr.state.Storage.GetStream(ctx, p.emoji.ImagePath)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return gtserror.Newf("error loading file from storage: %w", err)
 | 
							return gtserror.Newf("error loading file from storage: %w", err)
 | 
				
			||||||
| 
						 | 
					@ -293,32 +301,69 @@ func (p *ProcessingEmoji) finish(ctx context.Context) error {
 | 
				
			||||||
		return gtserror.Newf("error decoding image: %w", err)
 | 
							return gtserror.Newf("error decoding image: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// The image should be in-memory by now.
 | 
						// staticImg should be in-memory by
 | 
				
			||||||
 | 
						// now so we're done with storage.
 | 
				
			||||||
	if err := rc.Close(); err != nil {
 | 
						if err := rc.Close(); err != nil {
 | 
				
			||||||
		return gtserror.Newf("error closing file: %w", err)
 | 
							return gtserror.Newf("error closing file: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// This shouldn't already exist, but we do a check as it's worth logging.
 | 
						// Static img shouldn't exist in storage at this point,
 | 
				
			||||||
 | 
						// but we do a check as it's worth logging / cleaning up.
 | 
				
			||||||
	if have, _ := p.mgr.state.Storage.Has(ctx, p.emoji.ImageStaticPath); have {
 | 
						if have, _ := p.mgr.state.Storage.Has(ctx, p.emoji.ImageStaticPath); have {
 | 
				
			||||||
		log.Warnf(ctx, "static emoji already exists at storage path: %s", p.emoji.ImagePath)
 | 
							log.Warnf(ctx, "static emoji already exists at: %s", p.emoji.ImageStaticPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Attempt to remove static existing emoji at storage path (might be broken / out-of-date)
 | 
							// Attempt to remove existing thumbnail (might be broken / out-of-date).
 | 
				
			||||||
		if err := p.mgr.state.Storage.Delete(ctx, p.emoji.ImageStaticPath); err != nil {
 | 
							if err := p.mgr.state.Storage.Delete(ctx, p.emoji.ImageStaticPath); err != nil {
 | 
				
			||||||
			return gtserror.Newf("error removing static emoji from storage: %v", err)
 | 
								return gtserror.Newf("error removing static emoji %s from storage: %v", p.emoji.ImageStaticPath, err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Create an emoji PNG encoder stream.
 | 
						// Create emoji PNG encoder stream.
 | 
				
			||||||
	enc := staticImg.ToPNG()
 | 
						enc := staticImg.ToPNG()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Stream-encode the PNG static image into storage.
 | 
						// Stream-encode the PNG static emoji image into our storage driver.
 | 
				
			||||||
	sz, err := p.mgr.state.Storage.PutStream(ctx, p.emoji.ImageStaticPath, enc)
 | 
						sz, err := p.mgr.state.Storage.PutStream(ctx, p.emoji.ImageStaticPath, enc)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return gtserror.Newf("error stream-encoding static emoji to storage: %w", err)
 | 
							return gtserror.Newf("error stream-encoding static emoji to storage: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Set written image size.
 | 
						// Set final written thumb size.
 | 
				
			||||||
	p.emoji.ImageStaticFileSize = int(sz)
 | 
						p.emoji.ImageStaticFileSize = int(sz)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// cleanup will remove any traces of processing emoji from storage,
 | 
				
			||||||
 | 
					// and perform any other necessary cleanup steps after failure.
 | 
				
			||||||
 | 
					func (p *ProcessingEmoji) cleanup(ctx context.Context) {
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if p.emoji.ImagePath != "" {
 | 
				
			||||||
 | 
							// Ensure emoji file at path is deleted from storage.
 | 
				
			||||||
 | 
							err = p.mgr.state.Storage.Delete(ctx, p.emoji.ImagePath)
 | 
				
			||||||
 | 
							if err != nil && !storage.IsNotFound(err) {
 | 
				
			||||||
 | 
								log.Errorf(ctx, "error deleting %s: %v", p.emoji.ImagePath, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if p.emoji.ImageStaticPath != "" {
 | 
				
			||||||
 | 
							// Ensure emoji static file at path is deleted from storage.
 | 
				
			||||||
 | 
							err = p.mgr.state.Storage.Delete(ctx, p.emoji.ImageStaticPath)
 | 
				
			||||||
 | 
							if err != nil && !storage.IsNotFound(err) {
 | 
				
			||||||
 | 
								log.Errorf(ctx, "error deleting %s: %v", p.emoji.ImageStaticPath, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ensure marked as not cached.
 | 
				
			||||||
 | 
						p.emoji.Cached = util.Ptr(false)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// getInstanceAccountID determines the instance account ID from
 | 
				
			||||||
 | 
					// emoji static image storage path. returns false on failure.
 | 
				
			||||||
 | 
					func getInstanceAccountID(staticPath string) (string, bool) {
 | 
				
			||||||
 | 
						matches := regexes.FilePath.FindStringSubmatch(staticPath)
 | 
				
			||||||
 | 
						if len(matches) < 2 {
 | 
				
			||||||
 | 
							return "", false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return matches[1], true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,6 +19,7 @@ package media
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"bytes"
 | 
						"bytes"
 | 
				
			||||||
 | 
						"cmp"
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"image/jpeg"
 | 
						"image/jpeg"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
| 
						 | 
					@ -29,6 +30,7 @@ import (
 | 
				
			||||||
	terminator "codeberg.org/superseriousbusiness/exif-terminator"
 | 
						terminator "codeberg.org/superseriousbusiness/exif-terminator"
 | 
				
			||||||
	"github.com/disintegration/imaging"
 | 
						"github.com/disintegration/imaging"
 | 
				
			||||||
	"github.com/h2non/filetype"
 | 
						"github.com/h2non/filetype"
 | 
				
			||||||
 | 
						"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/gtserror"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/log"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/log"
 | 
				
			||||||
| 
						 | 
					@ -43,16 +45,14 @@ import (
 | 
				
			||||||
type ProcessingMedia struct {
 | 
					type ProcessingMedia struct {
 | 
				
			||||||
	media  *gtsmodel.MediaAttachment // processing media attachment details
 | 
						media  *gtsmodel.MediaAttachment // processing media attachment details
 | 
				
			||||||
	dataFn DataFunc                  // load-data function, returns media stream
 | 
						dataFn DataFunc                  // load-data function, returns media stream
 | 
				
			||||||
	recache bool                      // recaching existing (uncached) media
 | 
					 | 
				
			||||||
	done   bool                      // done is set when process finishes with non ctx canceled type error
 | 
						done   bool                      // done is set when process finishes with non ctx canceled type error
 | 
				
			||||||
	proc   runners.Processor         // proc helps synchronize only a singular running processing instance
 | 
						proc   runners.Processor         // proc helps synchronize only a singular running processing instance
 | 
				
			||||||
	err    error                     // error stores permanent error value when done
 | 
						err    error                     // error stores permanent error value when done
 | 
				
			||||||
	mgr    *Manager                  // mgr instance (access to db / storage)
 | 
						mgr    *Manager                  // mgr instance (access to db / storage)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// AttachmentID returns the ID of the underlying
 | 
					// ID returns the ID of the underlying media.
 | 
				
			||||||
// media attachment without blocking processing.
 | 
					func (p *ProcessingMedia) ID() string {
 | 
				
			||||||
func (p *ProcessingMedia) AttachmentID() string {
 | 
					 | 
				
			||||||
	return p.media.ID // immutable, safe outside mutex.
 | 
						return p.media.ID // immutable, safe outside mutex.
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -65,124 +65,102 @@ func (p *ProcessingMedia) AttachmentID() string {
 | 
				
			||||||
// will still be returned in that case, but it will
 | 
					// will still be returned in that case, but it will
 | 
				
			||||||
// only be partially complete and should be treated
 | 
					// only be partially complete and should be treated
 | 
				
			||||||
// as a placeholder.
 | 
					// as a placeholder.
 | 
				
			||||||
func (p *ProcessingMedia) LoadAttachment(ctx context.Context) (*gtsmodel.MediaAttachment, error) {
 | 
					func (p *ProcessingMedia) Load(ctx context.Context) (*gtsmodel.MediaAttachment, error) {
 | 
				
			||||||
	// Attempt to load synchronously.
 | 
					 | 
				
			||||||
	media, done, err := p.load(ctx)
 | 
						media, done, err := p.load(ctx)
 | 
				
			||||||
	if err == nil {
 | 
					 | 
				
			||||||
		// No issue, return media.
 | 
					 | 
				
			||||||
		return media, nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if !done {
 | 
						if !done {
 | 
				
			||||||
		// Provided context was cancelled,
 | 
							// On a context-canceled error (marked as !done), requeue for loading.
 | 
				
			||||||
		// e.g. request aborted early before
 | 
							p.mgr.state.Workers.Dereference.Queue.Push(func(ctx context.Context) {
 | 
				
			||||||
		// its context could be used to finish
 | 
								if _, _, err := p.load(ctx); err != nil {
 | 
				
			||||||
		// loading the attachment. Enqueue for
 | 
									log.Errorf(ctx, "error loading media: %v", err)
 | 
				
			||||||
		// asynchronous processing, which will
 | 
								}
 | 
				
			||||||
		// use a background context.
 | 
							})
 | 
				
			||||||
		log.Warnf(ctx, "reprocessing media %s after canceled ctx", p.media.ID)
 | 
					 | 
				
			||||||
		p.mgr.state.Workers.Media.Queue.Push(p.Process)
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Media could not be retrieved FULLY,
 | 
					 | 
				
			||||||
	// but partial attachment should be present.
 | 
					 | 
				
			||||||
	return media, err
 | 
						return media, err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Process allows the receiving object to fit the
 | 
					// load is the package private form of load() that is wrapped to catch context canceled.
 | 
				
			||||||
// runners.WorkerFunc signature. It performs a
 | 
					func (p *ProcessingMedia) load(ctx context.Context) (
 | 
				
			||||||
// (blocking) load and logs on error.
 | 
						media *gtsmodel.MediaAttachment,
 | 
				
			||||||
func (p *ProcessingMedia) Process(ctx context.Context) {
 | 
						done bool,
 | 
				
			||||||
	if _, _, err := p.load(ctx); err != nil {
 | 
						err error,
 | 
				
			||||||
		log.Errorf(ctx, "error(s) processing media: %v", err)
 | 
					) {
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// load performs a concurrency-safe load of ProcessingMedia, only
 | 
					 | 
				
			||||||
// marking itself as complete when returned error is NOT a context cancel.
 | 
					 | 
				
			||||||
func (p *ProcessingMedia) load(ctx context.Context) (*gtsmodel.MediaAttachment, bool, error) {
 | 
					 | 
				
			||||||
	var (
 | 
					 | 
				
			||||||
		done bool
 | 
					 | 
				
			||||||
		err  error
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	err = p.proc.Process(func() error {
 | 
						err = p.proc.Process(func() error {
 | 
				
			||||||
		if p.done {
 | 
							if done = p.done; done {
 | 
				
			||||||
			// Already proc'd.
 | 
								// Already proc'd.
 | 
				
			||||||
			return p.err
 | 
								return p.err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		defer func() {
 | 
							defer func() {
 | 
				
			||||||
			// This is only done when ctx NOT cancelled.
 | 
								// This is only done when ctx NOT cancelled.
 | 
				
			||||||
			done = err == nil || !errorsv2.IsV2(err,
 | 
								done = (err == nil || !errorsv2.IsV2(err,
 | 
				
			||||||
				context.Canceled,
 | 
									context.Canceled,
 | 
				
			||||||
				context.DeadlineExceeded,
 | 
									context.DeadlineExceeded,
 | 
				
			||||||
			)
 | 
								))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if !done {
 | 
								if !done {
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Anything from here, we
 | 
				
			||||||
 | 
								// need to ensure happens
 | 
				
			||||||
 | 
								// (i.e. no ctx canceled).
 | 
				
			||||||
 | 
								ctx = gtscontext.WithValues(
 | 
				
			||||||
 | 
									context.Background(),
 | 
				
			||||||
 | 
									ctx, // values
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// On error or unknown media types, perform error cleanup.
 | 
				
			||||||
 | 
								if err != nil || p.media.Type == gtsmodel.FileTypeUnknown {
 | 
				
			||||||
 | 
									p.cleanup(ctx)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Update with latest details, whatever happened.
 | 
				
			||||||
 | 
								e := p.mgr.state.DB.UpdateAttachment(ctx, p.media)
 | 
				
			||||||
 | 
								if e != nil {
 | 
				
			||||||
 | 
									log.Errorf(ctx, "error updating media in db: %v", e)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// Store final values.
 | 
								// Store final values.
 | 
				
			||||||
			p.done = true
 | 
								p.done = true
 | 
				
			||||||
			p.err = err
 | 
								p.err = err
 | 
				
			||||||
		}()
 | 
							}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Gather errors as we proceed.
 | 
							// TODO: in time update this
 | 
				
			||||||
		var errs = gtserror.NewMultiError(4)
 | 
							// to perhaps follow a similar
 | 
				
			||||||
 | 
							// freshness window to statuses
 | 
				
			||||||
 | 
							// / accounts? But that's a big
 | 
				
			||||||
 | 
							// maybe, media don't change in
 | 
				
			||||||
 | 
							// the same way so this is largely
 | 
				
			||||||
 | 
							// just to slow down fail retries.
 | 
				
			||||||
 | 
							const maxfreq = 6 * time.Hour
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Check whether media is uncached but repeatedly failing,
 | 
				
			||||||
 | 
							// specifically limit the frequency at which we allow this.
 | 
				
			||||||
 | 
							if !p.media.UpdatedAt.Equal(p.media.CreatedAt) && // i.e. not new
 | 
				
			||||||
 | 
								p.media.UpdatedAt.Add(maxfreq).Before(time.Now()) {
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Attempt to store media and calculate
 | 
							// Attempt to store media and calculate
 | 
				
			||||||
		// full-size media attachment details.
 | 
							// full-size media attachment details.
 | 
				
			||||||
		//
 | 
							//
 | 
				
			||||||
		// This will update p.media as it goes.
 | 
							// This will update p.media as it goes.
 | 
				
			||||||
		storeErr := p.store(ctx)
 | 
							if err = p.store(ctx); err != nil {
 | 
				
			||||||
		if storeErr != nil {
 | 
								return err
 | 
				
			||||||
			errs.Append(storeErr)
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Finish processing by reloading media into
 | 
							// Finish processing by reloading media into
 | 
				
			||||||
		// memory to get dimension and generate a thumb.
 | 
							// memory to get dimension and generate a thumb.
 | 
				
			||||||
		//
 | 
							//
 | 
				
			||||||
		// This will update p.media as it goes.
 | 
							// This will update p.media as it goes.
 | 
				
			||||||
		if finishErr := p.finish(ctx); finishErr != nil {
 | 
							if err = p.finish(ctx); err != nil {
 | 
				
			||||||
			errs.Append(finishErr)
 | 
								return err //nolint:revive
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// If this isn't a file we were able to process,
 | 
							return nil
 | 
				
			||||||
		// we may have partially stored it (eg., it's a
 | 
					 | 
				
			||||||
		// jpeg, which is fine, but streaming it to storage
 | 
					 | 
				
			||||||
		// was interrupted halfway through and so it was
 | 
					 | 
				
			||||||
		// never decoded). Try to clean up in this case.
 | 
					 | 
				
			||||||
		if p.media.Type == gtsmodel.FileTypeUnknown {
 | 
					 | 
				
			||||||
			deleteErr := p.mgr.state.Storage.Delete(ctx, p.media.File.Path)
 | 
					 | 
				
			||||||
			if deleteErr != nil && !storage.IsNotFound(deleteErr) {
 | 
					 | 
				
			||||||
				errs.Append(deleteErr)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		var dbErr error
 | 
					 | 
				
			||||||
		switch {
 | 
					 | 
				
			||||||
		case !p.recache:
 | 
					 | 
				
			||||||
			// First time caching this attachment, insert it.
 | 
					 | 
				
			||||||
			dbErr = p.mgr.state.DB.PutAttachment(ctx, p.media)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		case p.recache && len(errs) == 0:
 | 
					 | 
				
			||||||
			// Existing attachment we're recaching, update it.
 | 
					 | 
				
			||||||
			//
 | 
					 | 
				
			||||||
			// (We only want to update if everything went OK so far,
 | 
					 | 
				
			||||||
			// otherwise we'd better leave previous version alone.)
 | 
					 | 
				
			||||||
			dbErr = p.mgr.state.DB.UpdateAttachment(ctx, p.media)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if dbErr != nil {
 | 
					 | 
				
			||||||
			errs.Append(dbErr)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		err = errs.Combine()
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
						media = p.media
 | 
				
			||||||
	return p.media, done, err
 | 
						return
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// store calls the data function attached to p if it hasn't been called yet,
 | 
					// store calls the data function attached to p if it hasn't been called yet,
 | 
				
			||||||
| 
						 | 
					@ -231,10 +209,6 @@ func (p *ProcessingMedia) store(ctx context.Context) error {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Initial file size was misreported, so we didn't read
 | 
							// Initial file size was misreported, so we didn't read
 | 
				
			||||||
		// fully into hdrBuf. Reslice it to the size we did read.
 | 
							// fully into hdrBuf. Reslice it to the size we did read.
 | 
				
			||||||
		log.Warnf(ctx,
 | 
					 | 
				
			||||||
			"recovered from misreported file size; reported %d; read %d",
 | 
					 | 
				
			||||||
			fileSize, n,
 | 
					 | 
				
			||||||
		)
 | 
					 | 
				
			||||||
		hdrBuf = hdrBuf[:n]
 | 
							hdrBuf = hdrBuf[:n]
 | 
				
			||||||
		fileSize = n
 | 
							fileSize = n
 | 
				
			||||||
		p.media.File.FileSize = fileSize
 | 
							p.media.File.FileSize = fileSize
 | 
				
			||||||
| 
						 | 
					@ -273,20 +247,13 @@ func (p *ProcessingMedia) store(ctx context.Context) error {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	default:
 | 
						default:
 | 
				
			||||||
		// The file is not a supported format that
 | 
							// The file is not a supported format that we can process, so we can't do much with it.
 | 
				
			||||||
		// we can process, so we can't do much with it.
 | 
							log.Warnf(ctx, "unsupported media extension '%s'; not caching locally", info.Extension)
 | 
				
			||||||
		log.Warnf(ctx,
 | 
					 | 
				
			||||||
			"media extension '%s' not officially supported, will be processed as "+
 | 
					 | 
				
			||||||
				"type '%s' with minimal metadata, and will not be cached locally",
 | 
					 | 
				
			||||||
			info.Extension, gtsmodel.FileTypeUnknown,
 | 
					 | 
				
			||||||
		)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Don't bother storing this.
 | 
					 | 
				
			||||||
		store = false
 | 
							store = false
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Fill in correct attachment
 | 
						// Fill in correct attachment
 | 
				
			||||||
	// data now we're parsed it.
 | 
						// data now we've parsed it.
 | 
				
			||||||
	p.media.URL = uris.URIForAttachment(
 | 
						p.media.URL = uris.URIForAttachment(
 | 
				
			||||||
		p.media.AccountID,
 | 
							p.media.AccountID,
 | 
				
			||||||
		string(TypeAttachment),
 | 
							string(TypeAttachment),
 | 
				
			||||||
| 
						 | 
					@ -295,15 +262,11 @@ func (p *ProcessingMedia) store(ctx context.Context) error {
 | 
				
			||||||
		info.Extension,
 | 
							info.Extension,
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Prefer discovered mime type, fall back to
 | 
						// Prefer discovered MIME, fallback to generic data stream.
 | 
				
			||||||
	// generic "this contains some bytes" type.
 | 
						mime := cmp.Or(info.MIME.Value, "application/octet-stream")
 | 
				
			||||||
	mime := info.MIME.Value
 | 
					 | 
				
			||||||
	if mime == "" {
 | 
					 | 
				
			||||||
		mime = "application/octet-stream"
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	p.media.File.ContentType = mime
 | 
						p.media.File.ContentType = mime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Calculate attachment file path.
 | 
						// Calculate final media attachment file path.
 | 
				
			||||||
	p.media.File.Path = uris.StoragePathForAttachment(
 | 
						p.media.File.Path = uris.StoragePathForAttachment(
 | 
				
			||||||
		p.media.AccountID,
 | 
							p.media.AccountID,
 | 
				
			||||||
		string(TypeAttachment),
 | 
							string(TypeAttachment),
 | 
				
			||||||
| 
						 | 
					@ -323,23 +286,23 @@ func (p *ProcessingMedia) store(ctx context.Context) error {
 | 
				
			||||||
	// File shouldn't already exist in storage at this point,
 | 
						// File shouldn't already exist in storage at this point,
 | 
				
			||||||
	// but we do a check as it's worth logging / cleaning up.
 | 
						// but we do a check as it's worth logging / cleaning up.
 | 
				
			||||||
	if have, _ := p.mgr.state.Storage.Has(ctx, p.media.File.Path); have {
 | 
						if have, _ := p.mgr.state.Storage.Has(ctx, p.media.File.Path); have {
 | 
				
			||||||
		log.Warnf(ctx, "media already exists at storage path: %s", p.media.File.Path)
 | 
							log.Warnf(ctx, "media already exists at: %s", p.media.File.Path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Attempt to remove existing media at storage path (might be broken / out-of-date)
 | 
							// Attempt to remove existing media at storage path (might be broken / out-of-date)
 | 
				
			||||||
		if err := p.mgr.state.Storage.Delete(ctx, p.media.File.Path); err != nil {
 | 
							if err := p.mgr.state.Storage.Delete(ctx, p.media.File.Path); err != nil {
 | 
				
			||||||
			return gtserror.Newf("error removing media from storage: %v", err)
 | 
								return gtserror.Newf("error removing media %s from storage: %v", p.media.File.Path, err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Write the final reader stream to our storage.
 | 
						// Write the final reader stream to our storage driver.
 | 
				
			||||||
	wroteSize, err := p.mgr.state.Storage.PutStream(ctx, p.media.File.Path, r)
 | 
						sz, err = p.mgr.state.Storage.PutStream(ctx, p.media.File.Path, r)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return gtserror.Newf("error writing media to storage: %w", err)
 | 
							return gtserror.Newf("error writing media to storage: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Set actual written size
 | 
						// Set actual written size
 | 
				
			||||||
	// as authoritative file size.
 | 
						// as authoritative file size.
 | 
				
			||||||
	p.media.File.FileSize = int(wroteSize)
 | 
						p.media.File.FileSize = int(sz)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// We can now consider this cached.
 | 
						// We can now consider this cached.
 | 
				
			||||||
	p.media.Cached = util.Ptr(true)
 | 
						p.media.Cached = util.Ptr(true)
 | 
				
			||||||
| 
						 | 
					@ -348,36 +311,9 @@ func (p *ProcessingMedia) store(ctx context.Context) error {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (p *ProcessingMedia) finish(ctx context.Context) error {
 | 
					func (p *ProcessingMedia) finish(ctx context.Context) error {
 | 
				
			||||||
	// Make a jolly assumption about thumbnail type.
 | 
						// Nothing else to do if
 | 
				
			||||||
	p.media.Thumbnail.ContentType = mimeImageJpeg
 | 
						// media was not cached.
 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Calculate attachment thumbnail file path
 | 
					 | 
				
			||||||
	p.media.Thumbnail.Path = uris.StoragePathForAttachment(
 | 
					 | 
				
			||||||
		p.media.AccountID,
 | 
					 | 
				
			||||||
		string(TypeAttachment),
 | 
					 | 
				
			||||||
		string(SizeSmall),
 | 
					 | 
				
			||||||
		p.media.ID,
 | 
					 | 
				
			||||||
		// Always encode attachment
 | 
					 | 
				
			||||||
		// thumbnails as jpg.
 | 
					 | 
				
			||||||
		"jpg",
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Calculate attachment thumbnail serve path.
 | 
					 | 
				
			||||||
	p.media.Thumbnail.URL = uris.URIForAttachment(
 | 
					 | 
				
			||||||
		p.media.AccountID,
 | 
					 | 
				
			||||||
		string(TypeAttachment),
 | 
					 | 
				
			||||||
		string(SizeSmall),
 | 
					 | 
				
			||||||
		p.media.ID,
 | 
					 | 
				
			||||||
		// Always encode attachment
 | 
					 | 
				
			||||||
		// thumbnails as jpg.
 | 
					 | 
				
			||||||
		"jpg",
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// If original file hasn't been stored, there's
 | 
					 | 
				
			||||||
	// likely something wrong with the data, or we
 | 
					 | 
				
			||||||
	// don't want to store it. Skip everything else.
 | 
					 | 
				
			||||||
	if !*p.media.Cached {
 | 
						if !*p.media.Cached {
 | 
				
			||||||
		p.media.Processing = gtsmodel.ProcessingStatusProcessed
 | 
					 | 
				
			||||||
		return nil
 | 
							return nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -398,8 +334,7 @@ func (p *ProcessingMedia) finish(ctx context.Context) error {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// .jpeg, .gif, .webp image type
 | 
						// .jpeg, .gif, .webp image type
 | 
				
			||||||
	case mimeImageJpeg, mimeImageGif, mimeImageWebp:
 | 
						case mimeImageJpeg, mimeImageGif, mimeImageWebp:
 | 
				
			||||||
		fullImg, err = decodeImage(
 | 
							fullImg, err = decodeImage(rc,
 | 
				
			||||||
			rc,
 | 
					 | 
				
			||||||
			imaging.AutoOrientation(true),
 | 
								imaging.AutoOrientation(true),
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
| 
						 | 
					@ -451,9 +386,9 @@ func (p *ProcessingMedia) finish(ctx context.Context) error {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Set full-size dimensions in attachment info.
 | 
						// Set full-size dimensions in attachment info.
 | 
				
			||||||
	p.media.FileMeta.Original.Width = int(fullImg.Width())
 | 
						p.media.FileMeta.Original.Width = fullImg.Width()
 | 
				
			||||||
	p.media.FileMeta.Original.Height = int(fullImg.Height())
 | 
						p.media.FileMeta.Original.Height = fullImg.Height()
 | 
				
			||||||
	p.media.FileMeta.Original.Size = int(fullImg.Size())
 | 
						p.media.FileMeta.Original.Size = fullImg.Size()
 | 
				
			||||||
	p.media.FileMeta.Original.Aspect = fullImg.AspectRatio()
 | 
						p.media.FileMeta.Original.Aspect = fullImg.AspectRatio()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Get smaller thumbnail image
 | 
						// Get smaller thumbnail image
 | 
				
			||||||
| 
						 | 
					@ -475,44 +410,72 @@ func (p *ProcessingMedia) finish(ctx context.Context) error {
 | 
				
			||||||
		p.media.Blurhash = hash
 | 
							p.media.Blurhash = hash
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Thumbnail shouldn't already exist in storage at this point,
 | 
						// Thumbnail shouldn't exist in storage at this point,
 | 
				
			||||||
	// but we do a check as it's worth logging / cleaning up.
 | 
						// but we do a check as it's worth logging / cleaning up.
 | 
				
			||||||
	if have, _ := p.mgr.state.Storage.Has(ctx, p.media.Thumbnail.Path); have {
 | 
						if have, _ := p.mgr.state.Storage.Has(ctx, p.media.Thumbnail.Path); have {
 | 
				
			||||||
		log.Warnf(ctx, "thumbnail already exists at storage path: %s", p.media.Thumbnail.Path)
 | 
							log.Warnf(ctx, "thumbnail already exists at: %s", p.media.Thumbnail.Path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Attempt to remove existing thumbnail at storage path (might be broken / out-of-date)
 | 
							// Attempt to remove existing thumbnail (might be broken / out-of-date).
 | 
				
			||||||
		if err := p.mgr.state.Storage.Delete(ctx, p.media.Thumbnail.Path); err != nil {
 | 
							if err := p.mgr.state.Storage.Delete(ctx, p.media.Thumbnail.Path); err != nil {
 | 
				
			||||||
			return gtserror.Newf("error removing thumbnail from storage: %v", err)
 | 
								return gtserror.Newf("error removing thumbnail %s from storage: %v", p.media.Thumbnail.Path, err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Create a thumbnail JPEG encoder stream.
 | 
						// Create a thumbnail JPEG encoder stream.
 | 
				
			||||||
	enc := thumbImg.ToJPEG(&jpeg.Options{
 | 
						enc := thumbImg.ToJPEG(&jpeg.Options{
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Good enough for
 | 
							// Good enough for
 | 
				
			||||||
		// a thumbnail.
 | 
							// a thumbnail.
 | 
				
			||||||
		Quality: 70,
 | 
							Quality: 70,
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Stream-encode the JPEG thumbnail image into storage.
 | 
						// Stream-encode the JPEG thumbnail image into our storage driver.
 | 
				
			||||||
	sz, err := p.mgr.state.Storage.PutStream(ctx, p.media.Thumbnail.Path, enc)
 | 
						sz, err := p.mgr.state.Storage.PutStream(ctx, p.media.Thumbnail.Path, enc)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return gtserror.Newf("error stream-encoding thumbnail to storage: %w", err)
 | 
							return gtserror.Newf("error stream-encoding thumbnail to storage: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Set final written thumb size.
 | 
				
			||||||
 | 
						p.media.Thumbnail.FileSize = int(sz)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Set thumbnail dimensions in attachment info.
 | 
						// Set thumbnail dimensions in attachment info.
 | 
				
			||||||
	p.media.FileMeta.Small = gtsmodel.Small{
 | 
						p.media.FileMeta.Small = gtsmodel.Small{
 | 
				
			||||||
		Width:  int(thumbImg.Width()),
 | 
							Width:  thumbImg.Width(),
 | 
				
			||||||
		Height: int(thumbImg.Height()),
 | 
							Height: thumbImg.Height(),
 | 
				
			||||||
		Size:   int(thumbImg.Size()),
 | 
							Size:   thumbImg.Size(),
 | 
				
			||||||
		Aspect: thumbImg.AspectRatio(),
 | 
							Aspect: thumbImg.AspectRatio(),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Set written image size.
 | 
						// Finally set the attachment as processed.
 | 
				
			||||||
	p.media.Thumbnail.FileSize = int(sz)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Finally set the attachment as processed and update time.
 | 
					 | 
				
			||||||
	p.media.Processing = gtsmodel.ProcessingStatusProcessed
 | 
						p.media.Processing = gtsmodel.ProcessingStatusProcessed
 | 
				
			||||||
	p.media.File.UpdatedAt = time.Now()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// cleanup will remove any traces of processing media from storage.
 | 
				
			||||||
 | 
					// and perform any other necessary cleanup steps after failure.
 | 
				
			||||||
 | 
					func (p *ProcessingMedia) cleanup(ctx context.Context) {
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if p.media.File.Path != "" {
 | 
				
			||||||
 | 
							// Ensure media file at path is deleted from storage.
 | 
				
			||||||
 | 
							err = p.mgr.state.Storage.Delete(ctx, p.media.File.Path)
 | 
				
			||||||
 | 
							if err != nil && !storage.IsNotFound(err) {
 | 
				
			||||||
 | 
								log.Errorf(ctx, "error deleting %s: %v", p.media.File.Path, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if p.media.Thumbnail.Path != "" {
 | 
				
			||||||
 | 
							// Ensure media thumbnail at path is deleted from storage.
 | 
				
			||||||
 | 
							err = p.mgr.state.Storage.Delete(ctx, p.media.Thumbnail.Path)
 | 
				
			||||||
 | 
							if err != nil && !storage.IsNotFound(err) {
 | 
				
			||||||
 | 
								log.Errorf(ctx, "error deleting %s: %v", p.media.Thumbnail.Path, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Also ensure marked as unknown and finished
 | 
				
			||||||
 | 
						// processing so gets inserted as placeholder URL.
 | 
				
			||||||
 | 
						p.media.Processing = gtsmodel.ProcessingStatusProcessed
 | 
				
			||||||
 | 
						p.media.Type = gtsmodel.FileTypeUnknown
 | 
				
			||||||
 | 
						p.media.Cached = util.Ptr(false)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -112,19 +112,19 @@ func (m *Manager) RefetchEmojis(ctx context.Context, domain string, dereferenceM
 | 
				
			||||||
			return dereferenceMedia(ctx, emojiImageIRI)
 | 
								return dereferenceMedia(ctx, emojiImageIRI)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		processingEmoji, err := m.PreProcessEmoji(ctx, dataFunc, emoji.Shortcode, emoji.ID, emoji.URI, &AdditionalEmojiInfo{
 | 
							processingEmoji, err := m.RefreshEmoji(ctx, emoji, dataFunc, AdditionalEmojiInfo{
 | 
				
			||||||
			Domain:               &emoji.Domain,
 | 
								Domain:               &emoji.Domain,
 | 
				
			||||||
			ImageRemoteURL:       &emoji.ImageRemoteURL,
 | 
								ImageRemoteURL:       &emoji.ImageRemoteURL,
 | 
				
			||||||
			ImageStaticRemoteURL: &emoji.ImageStaticRemoteURL,
 | 
								ImageStaticRemoteURL: &emoji.ImageStaticRemoteURL,
 | 
				
			||||||
			Disabled:             emoji.Disabled,
 | 
								Disabled:             emoji.Disabled,
 | 
				
			||||||
			VisibleInPicker:      emoji.VisibleInPicker,
 | 
								VisibleInPicker:      emoji.VisibleInPicker,
 | 
				
			||||||
		}, true)
 | 
							})
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			log.Errorf(ctx, "emoji %s could not be refreshed because of an error during processing: %s", shortcodeDomain, err)
 | 
								log.Errorf(ctx, "emoji %s could not be refreshed because of an error during processing: %s", shortcodeDomain, err)
 | 
				
			||||||
			continue
 | 
								continue
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if _, err := processingEmoji.LoadEmoji(ctx); err != nil {
 | 
							if _, err := processingEmoji.Load(ctx); err != nil {
 | 
				
			||||||
			log.Errorf(ctx, "emoji %s could not be refreshed because of an error during loading: %s", shortcodeDomain, err)
 | 
								log.Errorf(ctx, "emoji %s could not be refreshed because of an error during loading: %s", shortcodeDomain, err)
 | 
				
			||||||
			continue
 | 
								continue
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -61,47 +61,85 @@ const (
 | 
				
			||||||
	TypeEmoji      Type = "emoji"      // TypeEmoji is the key for emoji type requests
 | 
						TypeEmoji      Type = "emoji"      // TypeEmoji is the key for emoji type requests
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// AdditionalMediaInfo represents additional information that should be added to an attachment
 | 
					// AdditionalMediaInfo represents additional information that
 | 
				
			||||||
// when processing a piece of media.
 | 
					// should be added to attachment when processing a piece of media.
 | 
				
			||||||
type AdditionalMediaInfo struct {
 | 
					type AdditionalMediaInfo struct {
 | 
				
			||||||
	// Time that this media was created; defaults to time.Now().
 | 
					
 | 
				
			||||||
 | 
						// Time that this media was
 | 
				
			||||||
 | 
						// created; defaults to time.Now().
 | 
				
			||||||
	CreatedAt *time.Time
 | 
						CreatedAt *time.Time
 | 
				
			||||||
	// ID of the status to which this media is attached; defaults to "".
 | 
					
 | 
				
			||||||
 | 
						// ID of the status to which this
 | 
				
			||||||
 | 
						// media is attached; defaults to "".
 | 
				
			||||||
	StatusID *string
 | 
						StatusID *string
 | 
				
			||||||
	// URL of the media on a remote instance; defaults to "".
 | 
					
 | 
				
			||||||
 | 
						// URL of the media on a
 | 
				
			||||||
 | 
						// remote instance; defaults to "".
 | 
				
			||||||
	RemoteURL *string
 | 
						RemoteURL *string
 | 
				
			||||||
	// Image description of this media; defaults to "".
 | 
					
 | 
				
			||||||
 | 
						// Image description of
 | 
				
			||||||
 | 
						// this media; defaults to "".
 | 
				
			||||||
	Description *string
 | 
						Description *string
 | 
				
			||||||
	// Blurhash of this media; defaults to "".
 | 
					
 | 
				
			||||||
 | 
						// Blurhash of this
 | 
				
			||||||
 | 
						// media; defaults to "".
 | 
				
			||||||
	Blurhash *string
 | 
						Blurhash *string
 | 
				
			||||||
	// ID of the scheduled status to which this media is attached; defaults to "".
 | 
					
 | 
				
			||||||
 | 
						// ID of the scheduled status to which
 | 
				
			||||||
 | 
						// this media is attached; defaults to "".
 | 
				
			||||||
	ScheduledStatusID *string
 | 
						ScheduledStatusID *string
 | 
				
			||||||
	// Mark this media as in-use as an avatar; defaults to false.
 | 
					
 | 
				
			||||||
 | 
						// Mark this media as in-use
 | 
				
			||||||
 | 
						// as an avatar; defaults to false.
 | 
				
			||||||
	Avatar *bool
 | 
						Avatar *bool
 | 
				
			||||||
	// Mark this media as in-use as a header; defaults to false.
 | 
					
 | 
				
			||||||
 | 
						// Mark this media as in-use
 | 
				
			||||||
 | 
						// as a header; defaults to false.
 | 
				
			||||||
	Header *bool
 | 
						Header *bool
 | 
				
			||||||
	// X focus coordinate for this media; defaults to 0.
 | 
					
 | 
				
			||||||
 | 
						// X focus coordinate for
 | 
				
			||||||
 | 
						// this media; defaults to 0.
 | 
				
			||||||
	FocusX *float32
 | 
						FocusX *float32
 | 
				
			||||||
	// Y focus coordinate for this media; defaults to 0.
 | 
					
 | 
				
			||||||
 | 
						// Y focus coordinate for
 | 
				
			||||||
 | 
						// this media; defaults to 0.
 | 
				
			||||||
	FocusY *float32
 | 
						FocusY *float32
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// AdditionalEmojiInfo represents additional information
 | 
					// AdditionalEmojiInfo represents additional information
 | 
				
			||||||
// that should be taken into account when processing an emoji.
 | 
					// that should be taken into account when processing an emoji.
 | 
				
			||||||
type AdditionalEmojiInfo struct {
 | 
					type AdditionalEmojiInfo struct {
 | 
				
			||||||
	// Time that this emoji was created; defaults to time.Now().
 | 
					
 | 
				
			||||||
 | 
						// ActivityPub URI of
 | 
				
			||||||
 | 
						// this remote emoji.
 | 
				
			||||||
 | 
						URI *string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Time that this emoji was
 | 
				
			||||||
 | 
						// created; defaults to time.Now().
 | 
				
			||||||
	CreatedAt *time.Time
 | 
						CreatedAt *time.Time
 | 
				
			||||||
	// Domain the emoji originated from. Blank for this instance's domain. Defaults to "".
 | 
					
 | 
				
			||||||
 | 
						// Domain the emoji originated from. Blank
 | 
				
			||||||
 | 
						// for this instance's domain. Defaults to "".
 | 
				
			||||||
	Domain *string
 | 
						Domain *string
 | 
				
			||||||
	// URL of this emoji on a remote instance; defaults to "".
 | 
					
 | 
				
			||||||
 | 
						// URL of this emoji on a
 | 
				
			||||||
 | 
						// remote instance; defaults to "".
 | 
				
			||||||
	ImageRemoteURL *string
 | 
						ImageRemoteURL *string
 | 
				
			||||||
	// URL of the static version of this emoji on a remote instance; defaults to "".
 | 
					
 | 
				
			||||||
 | 
						// URL of the static version of this emoji
 | 
				
			||||||
 | 
						// on a remote instance; defaults to "".
 | 
				
			||||||
	ImageStaticRemoteURL *string
 | 
						ImageStaticRemoteURL *string
 | 
				
			||||||
	// Whether this emoji should be disabled (not shown) on this instance; defaults to false.
 | 
					
 | 
				
			||||||
 | 
						// Whether this emoji should be disabled (not
 | 
				
			||||||
 | 
						// shown) on this instance; defaults to false.
 | 
				
			||||||
	Disabled *bool
 | 
						Disabled *bool
 | 
				
			||||||
	// Whether this emoji should be visible in the instance's emoji picker; defaults to true.
 | 
					
 | 
				
			||||||
 | 
						// Whether this emoji should be visible in
 | 
				
			||||||
 | 
						// the instance's emoji picker; defaults to true.
 | 
				
			||||||
	VisibleInPicker *bool
 | 
						VisibleInPicker *bool
 | 
				
			||||||
	// ID of the category this emoji should be placed in; defaults to "".
 | 
					
 | 
				
			||||||
 | 
						// ID of the category this emoji
 | 
				
			||||||
 | 
						// should be placed in; defaults to "".
 | 
				
			||||||
	CategoryID *string
 | 
						CategoryID *string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,6 +37,5 @@ func newHdrBuf(fileSize int) []byte {
 | 
				
			||||||
	if fileSize > 0 && fileSize < bufSize {
 | 
						if fileSize > 0 && fileSize < bufSize {
 | 
				
			||||||
		bufSize = fileSize
 | 
							bufSize = fileSize
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
	return make([]byte, bufSize)
 | 
						return make([]byte, bufSize)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -111,7 +111,7 @@ func (suite *AccountStandardTestSuite) SetupTest() {
 | 
				
			||||||
	suite.emailSender = testrig.NewEmailSender("../../../web/template/", suite.sentEmails)
 | 
						suite.emailSender = testrig.NewEmailSender("../../../web/template/", suite.sentEmails)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	filter := visibility.NewFilter(&suite.state)
 | 
						filter := visibility.NewFilter(&suite.state)
 | 
				
			||||||
	common := common.New(&suite.state, suite.tc, suite.federator, filter)
 | 
						common := common.New(&suite.state, suite.mediaManager, suite.tc, suite.federator, filter)
 | 
				
			||||||
	suite.accountProcessor = account.New(&common, &suite.state, suite.tc, suite.mediaManager, suite.federator, filter, processing.GetParseMentionFunc(&suite.state, suite.federator))
 | 
						suite.accountProcessor = account.New(&common, &suite.state, suite.tc, suite.mediaManager, suite.federator, filter, processing.GetParseMentionFunc(&suite.state, suite.federator))
 | 
				
			||||||
	testrig.StandardDBSetup(suite.db, nil)
 | 
						testrig.StandardDBSetup(suite.db, nil)
 | 
				
			||||||
	testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
 | 
						testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,10 +19,12 @@ package account
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"mime/multipart"
 | 
						"mime/multipart"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"codeberg.org/gruf/go-bytesize"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/ap"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/ap"
 | 
				
			||||||
	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
 | 
						apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/config"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/config"
 | 
				
			||||||
| 
						 | 
					@ -203,9 +205,13 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if form.Avatar != nil && form.Avatar.Size != 0 {
 | 
						if form.Avatar != nil && form.Avatar.Size != 0 {
 | 
				
			||||||
		avatarInfo, err := p.UpdateAvatar(ctx, form.Avatar, nil, account.ID)
 | 
							avatarInfo, errWithCode := p.UpdateAvatar(ctx,
 | 
				
			||||||
		if err != nil {
 | 
								account,
 | 
				
			||||||
			return nil, gtserror.NewErrorBadRequest(err)
 | 
								form.Avatar,
 | 
				
			||||||
 | 
								nil,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							if errWithCode != nil {
 | 
				
			||||||
 | 
								return nil, errWithCode
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		account.AvatarMediaAttachmentID = avatarInfo.ID
 | 
							account.AvatarMediaAttachmentID = avatarInfo.ID
 | 
				
			||||||
		account.AvatarMediaAttachment = avatarInfo
 | 
							account.AvatarMediaAttachment = avatarInfo
 | 
				
			||||||
| 
						 | 
					@ -213,9 +219,13 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if form.Header != nil && form.Header.Size != 0 {
 | 
						if form.Header != nil && form.Header.Size != 0 {
 | 
				
			||||||
		headerInfo, err := p.UpdateHeader(ctx, form.Header, nil, account.ID)
 | 
							headerInfo, errWithCode := p.UpdateHeader(ctx,
 | 
				
			||||||
		if err != nil {
 | 
								account,
 | 
				
			||||||
			return nil, gtserror.NewErrorBadRequest(err)
 | 
								form.Header,
 | 
				
			||||||
 | 
								nil,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							if errWithCode != nil {
 | 
				
			||||||
 | 
								return nil, errWithCode
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		account.HeaderMediaAttachmentID = headerInfo.ID
 | 
							account.HeaderMediaAttachmentID = headerInfo.ID
 | 
				
			||||||
		account.HeaderMediaAttachment = headerInfo
 | 
							account.HeaderMediaAttachment = headerInfo
 | 
				
			||||||
| 
						 | 
					@ -316,35 +326,33 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
 | 
				
			||||||
// for this to become the account's new avatar.
 | 
					// for this to become the account's new avatar.
 | 
				
			||||||
func (p *Processor) UpdateAvatar(
 | 
					func (p *Processor) UpdateAvatar(
 | 
				
			||||||
	ctx context.Context,
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						account *gtsmodel.Account,
 | 
				
			||||||
	avatar *multipart.FileHeader,
 | 
						avatar *multipart.FileHeader,
 | 
				
			||||||
	description *string,
 | 
						description *string,
 | 
				
			||||||
	accountID string,
 | 
					) (
 | 
				
			||||||
) (*gtsmodel.MediaAttachment, error) {
 | 
						*gtsmodel.MediaAttachment,
 | 
				
			||||||
	maxImageSize := config.GetMediaImageMaxSize()
 | 
						gtserror.WithCode,
 | 
				
			||||||
	if avatar.Size > int64(maxImageSize) {
 | 
					) {
 | 
				
			||||||
		return nil, gtserror.Newf("size %d exceeded max media size of %d bytes", avatar.Size, maxImageSize)
 | 
						max := config.GetMediaImageMaxSize()
 | 
				
			||||||
 | 
						if sz := bytesize.Size(avatar.Size); sz > max {
 | 
				
			||||||
 | 
							text := fmt.Sprintf("size %s exceeds max media size %s", sz, max)
 | 
				
			||||||
 | 
							return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	data := func(innerCtx context.Context) (io.ReadCloser, int64, error) {
 | 
						data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
				
			||||||
		f, err := avatar.Open()
 | 
							f, err := avatar.Open()
 | 
				
			||||||
		return f, avatar.Size, err
 | 
							return f, avatar.Size, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Process the media attachment and load it immediately.
 | 
						// Write to instance storage.
 | 
				
			||||||
	media := p.mediaManager.PreProcessMedia(data, accountID, &media.AdditionalMediaInfo{
 | 
						return p.c.StoreLocalMedia(ctx,
 | 
				
			||||||
 | 
							account.ID,
 | 
				
			||||||
 | 
							data,
 | 
				
			||||||
 | 
							media.AdditionalMediaInfo{
 | 
				
			||||||
			Avatar:      util.Ptr(true),
 | 
								Avatar:      util.Ptr(true),
 | 
				
			||||||
			Description: description,
 | 
								Description: description,
 | 
				
			||||||
	})
 | 
							},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	attachment, err := media.LoadAttachment(ctx)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
 | 
					 | 
				
			||||||
	} else if attachment.Type == gtsmodel.FileTypeUnknown {
 | 
					 | 
				
			||||||
		err = gtserror.Newf("could not process uploaded file with extension %s", attachment.File.ContentType)
 | 
					 | 
				
			||||||
		return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return attachment, nil
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// UpdateHeader does the dirty work of checking the header
 | 
					// UpdateHeader does the dirty work of checking the header
 | 
				
			||||||
| 
						 | 
					@ -353,33 +361,31 @@ func (p *Processor) UpdateAvatar(
 | 
				
			||||||
// for this to become the account's new header.
 | 
					// for this to become the account's new header.
 | 
				
			||||||
func (p *Processor) UpdateHeader(
 | 
					func (p *Processor) UpdateHeader(
 | 
				
			||||||
	ctx context.Context,
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						account *gtsmodel.Account,
 | 
				
			||||||
	header *multipart.FileHeader,
 | 
						header *multipart.FileHeader,
 | 
				
			||||||
	description *string,
 | 
						description *string,
 | 
				
			||||||
	accountID string,
 | 
					) (
 | 
				
			||||||
) (*gtsmodel.MediaAttachment, error) {
 | 
						*gtsmodel.MediaAttachment,
 | 
				
			||||||
	maxImageSize := config.GetMediaImageMaxSize()
 | 
						gtserror.WithCode,
 | 
				
			||||||
	if header.Size > int64(maxImageSize) {
 | 
					) {
 | 
				
			||||||
		return nil, gtserror.Newf("size %d exceeded max media size of %d bytes", header.Size, maxImageSize)
 | 
						max := config.GetMediaImageMaxSize()
 | 
				
			||||||
 | 
						if sz := bytesize.Size(header.Size); sz > max {
 | 
				
			||||||
 | 
							text := fmt.Sprintf("size %s exceeds max media size %s", sz, max)
 | 
				
			||||||
 | 
							return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	data := func(innerCtx context.Context) (io.ReadCloser, int64, error) {
 | 
						data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
				
			||||||
		f, err := header.Open()
 | 
							f, err := header.Open()
 | 
				
			||||||
		return f, header.Size, err
 | 
							return f, header.Size, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Process the media attachment and load it immediately.
 | 
						// Write to instance storage.
 | 
				
			||||||
	media := p.mediaManager.PreProcessMedia(data, accountID, &media.AdditionalMediaInfo{
 | 
						return p.c.StoreLocalMedia(ctx,
 | 
				
			||||||
 | 
							account.ID,
 | 
				
			||||||
 | 
							data,
 | 
				
			||||||
 | 
							media.AdditionalMediaInfo{
 | 
				
			||||||
			Header:      util.Ptr(true),
 | 
								Header:      util.Ptr(true),
 | 
				
			||||||
			Description: description,
 | 
								Description: description,
 | 
				
			||||||
	})
 | 
							},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	attachment, err := media.LoadAttachment(ctx)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
 | 
					 | 
				
			||||||
	} else if attachment.Type == gtsmodel.FileTypeUnknown {
 | 
					 | 
				
			||||||
		err = gtserror.Newf("could not process uploaded file with extension %s", attachment.File.ContentType)
 | 
					 | 
				
			||||||
		return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return attachment, nil
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,20 +20,26 @@ package admin
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/cleaner"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/cleaner"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/email"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/email"
 | 
				
			||||||
 | 
						"github.com/superseriousbusiness/gotosocial/internal/federation"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/media"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/media"
 | 
				
			||||||
 | 
						"github.com/superseriousbusiness/gotosocial/internal/processing/common"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/state"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/state"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/transport"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/transport"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/typeutils"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/typeutils"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Processor struct {
 | 
					type Processor struct {
 | 
				
			||||||
 | 
						// common processor logic
 | 
				
			||||||
 | 
						c *common.Processor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	state     *state.State
 | 
						state     *state.State
 | 
				
			||||||
	cleaner   *cleaner.Cleaner
 | 
						cleaner   *cleaner.Cleaner
 | 
				
			||||||
	converter *typeutils.Converter
 | 
						converter *typeutils.Converter
 | 
				
			||||||
	mediaManager        *media.Manager
 | 
						federator *federation.Federator
 | 
				
			||||||
	transportController transport.Controller
 | 
						media     *media.Manager
 | 
				
			||||||
	emailSender         email.Sender
 | 
						transport transport.Controller
 | 
				
			||||||
 | 
						email     email.Sender
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// admin Actions currently
 | 
						// admin Actions currently
 | 
				
			||||||
	// undergoing processing
 | 
						// undergoing processing
 | 
				
			||||||
| 
						 | 
					@ -46,21 +52,24 @@ func (p *Processor) Actions() *Actions {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// New returns a new admin processor.
 | 
					// New returns a new admin processor.
 | 
				
			||||||
func New(
 | 
					func New(
 | 
				
			||||||
 | 
						common *common.Processor,
 | 
				
			||||||
	state *state.State,
 | 
						state *state.State,
 | 
				
			||||||
	cleaner *cleaner.Cleaner,
 | 
						cleaner *cleaner.Cleaner,
 | 
				
			||||||
 | 
						federator *federation.Federator,
 | 
				
			||||||
	converter *typeutils.Converter,
 | 
						converter *typeutils.Converter,
 | 
				
			||||||
	mediaManager *media.Manager,
 | 
						mediaManager *media.Manager,
 | 
				
			||||||
	transportController transport.Controller,
 | 
						transportController transport.Controller,
 | 
				
			||||||
	emailSender email.Sender,
 | 
						emailSender email.Sender,
 | 
				
			||||||
) Processor {
 | 
					) Processor {
 | 
				
			||||||
	return Processor{
 | 
						return Processor{
 | 
				
			||||||
 | 
							c:         common,
 | 
				
			||||||
		state:     state,
 | 
							state:     state,
 | 
				
			||||||
		cleaner:   cleaner,
 | 
							cleaner:   cleaner,
 | 
				
			||||||
		converter: converter,
 | 
							converter: converter,
 | 
				
			||||||
		mediaManager:        mediaManager,
 | 
							federator: federator,
 | 
				
			||||||
		transportController: transportController,
 | 
							media:     mediaManager,
 | 
				
			||||||
		emailSender:         emailSender,
 | 
							transport: transportController,
 | 
				
			||||||
 | 
							email:     emailSender,
 | 
				
			||||||
		actions: &Actions{
 | 
							actions: &Actions{
 | 
				
			||||||
			r:     make(map[string]*gtsmodel.AdminAction),
 | 
								r:     make(map[string]*gtsmodel.AdminAction),
 | 
				
			||||||
			state: state,
 | 
								state: state,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -78,7 +78,7 @@ func (p *Processor) DebugAPUrl(
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// All looks fine. Prepare the transport and (signed) GET request.
 | 
						// All looks fine. Prepare the transport and (signed) GET request.
 | 
				
			||||||
	tsport, err := p.transportController.NewTransportForUsername(ctx, adminAcct.Username)
 | 
						tsport, err := p.transport.NewTransportForUsername(ctx, adminAcct.Username)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		err = gtserror.Newf("error creating transport: %w", err)
 | 
							err = gtserror.Newf("error creating transport: %w", err)
 | 
				
			||||||
		return nil, gtserror.NewErrorInternalError(err, err.Error())
 | 
							return nil, gtserror.NewErrorInternalError(err, err.Error())
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -55,7 +55,7 @@ func (p *Processor) EmailTest(
 | 
				
			||||||
		InstanceName:    instance.Title,
 | 
							InstanceName:    instance.Title,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := p.emailSender.SendTestEmail(toAddress, testData); err != nil {
 | 
						if err := p.email.SendTestEmail(toAddress, testData); err != nil {
 | 
				
			||||||
		if gtserror.IsSMTP(err) {
 | 
							if gtserror.IsSMTP(err) {
 | 
				
			||||||
			// An error occurred during the SMTP part.
 | 
								// An error occurred during the SMTP part.
 | 
				
			||||||
			// We should indicate this to the caller, as
 | 
								// We should indicate this to the caller, as
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -31,7 +31,6 @@ import (
 | 
				
			||||||
	"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/media"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/media"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/uris"
 | 
					 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/util"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/util"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -41,64 +40,21 @@ func (p *Processor) EmojiCreate(
 | 
				
			||||||
	account *gtsmodel.Account,
 | 
						account *gtsmodel.Account,
 | 
				
			||||||
	form *apimodel.EmojiCreateRequest,
 | 
						form *apimodel.EmojiCreateRequest,
 | 
				
			||||||
) (*apimodel.Emoji, gtserror.WithCode) {
 | 
					) (*apimodel.Emoji, gtserror.WithCode) {
 | 
				
			||||||
	// Ensure emoji with this shortcode
 | 
					 | 
				
			||||||
	// doesn't already exist on the instance.
 | 
					 | 
				
			||||||
	maybeExisting, err := p.state.DB.GetEmojiByShortcodeDomain(ctx, form.Shortcode, "")
 | 
					 | 
				
			||||||
	if err != nil && !errors.Is(err, db.ErrNoEntries) {
 | 
					 | 
				
			||||||
		err := gtserror.Newf("error checking existence of emoji with shortcode %s: %w", form.Shortcode, err)
 | 
					 | 
				
			||||||
		return nil, gtserror.NewErrorInternalError(err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if maybeExisting != nil {
 | 
						// Simply read provided form data for emoji data source.
 | 
				
			||||||
		err := fmt.Errorf("emoji with shortcode %s already exists", form.Shortcode)
 | 
						data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
				
			||||||
		return nil, gtserror.NewErrorConflict(err, err.Error())
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Prepare data function for emoji processing
 | 
					 | 
				
			||||||
	// (just read data from the submitted form).
 | 
					 | 
				
			||||||
	data := func(innerCtx context.Context) (io.ReadCloser, int64, error) {
 | 
					 | 
				
			||||||
		f, err := form.Image.Open()
 | 
							f, err := form.Image.Open()
 | 
				
			||||||
		return f, form.Image.Size, err
 | 
							return f, form.Image.Size, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// If category was supplied on the form,
 | 
						// Attempt to create the new local emoji.
 | 
				
			||||||
	// ensure the category exists and provide
 | 
						emoji, errWithCode := p.createEmoji(ctx,
 | 
				
			||||||
	// it as additional info to emoji processing.
 | 
							form.Shortcode,
 | 
				
			||||||
	var ai *media.AdditionalEmojiInfo
 | 
							form.CategoryName,
 | 
				
			||||||
	if form.CategoryName != "" {
 | 
							data,
 | 
				
			||||||
		category, err := p.getOrCreateEmojiCategory(ctx, form.CategoryName)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return nil, gtserror.NewErrorInternalError(err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		ai = &media.AdditionalEmojiInfo{
 | 
					 | 
				
			||||||
			CategoryID: &category.ID,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Generate new emoji ID and URI.
 | 
					 | 
				
			||||||
	emojiID, err := id.NewRandomULID()
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		err := gtserror.Newf("error creating id for new emoji: %w", err)
 | 
					 | 
				
			||||||
		return nil, gtserror.NewErrorInternalError(err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	emojiURI := uris.URIForEmoji(emojiID)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Begin media processing.
 | 
					 | 
				
			||||||
	processingEmoji, err := p.mediaManager.PreProcessEmoji(ctx,
 | 
					 | 
				
			||||||
		data, form.Shortcode, emojiID, emojiURI, ai, false,
 | 
					 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	if err != nil {
 | 
						if errWithCode != nil {
 | 
				
			||||||
		err := gtserror.Newf("error processing emoji: %w", err)
 | 
							return nil, errWithCode
 | 
				
			||||||
		return nil, gtserror.NewErrorInternalError(err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Complete processing immediately.
 | 
					 | 
				
			||||||
	emoji, err := processingEmoji.LoadEmoji(ctx)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		err := gtserror.Newf("error loading emoji: %w", err)
 | 
					 | 
				
			||||||
		return nil, gtserror.NewErrorInternalError(err)
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	apiEmoji, err := p.converter.EmojiToAPIEmoji(ctx, emoji)
 | 
						apiEmoji, err := p.converter.EmojiToAPIEmoji(ctx, emoji)
 | 
				
			||||||
| 
						 | 
					@ -110,53 +66,6 @@ func (p *Processor) EmojiCreate(
 | 
				
			||||||
	return &apiEmoji, nil
 | 
						return &apiEmoji, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// emojisGetFilterParams builds extra
 | 
					 | 
				
			||||||
// query parameters to return as part
 | 
					 | 
				
			||||||
// of an Emojis pageable response.
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// The returned string will look like:
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// "filter=domain:all,enabled,shortcode:example"
 | 
					 | 
				
			||||||
func emojisGetFilterParams(
 | 
					 | 
				
			||||||
	shortcode string,
 | 
					 | 
				
			||||||
	domain string,
 | 
					 | 
				
			||||||
	includeDisabled bool,
 | 
					 | 
				
			||||||
	includeEnabled bool,
 | 
					 | 
				
			||||||
) string {
 | 
					 | 
				
			||||||
	var filterBuilder strings.Builder
 | 
					 | 
				
			||||||
	filterBuilder.WriteString("filter=")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	switch domain {
 | 
					 | 
				
			||||||
	case "", "local":
 | 
					 | 
				
			||||||
		// Local emojis only.
 | 
					 | 
				
			||||||
		filterBuilder.WriteString("domain:local")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	case db.EmojiAllDomains:
 | 
					 | 
				
			||||||
		// Local or remote.
 | 
					 | 
				
			||||||
		filterBuilder.WriteString("domain:all")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	default:
 | 
					 | 
				
			||||||
		// Specific domain only.
 | 
					 | 
				
			||||||
		filterBuilder.WriteString("domain:" + domain)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if includeDisabled != includeEnabled {
 | 
					 | 
				
			||||||
		if includeDisabled {
 | 
					 | 
				
			||||||
			filterBuilder.WriteString(",disabled")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if includeEnabled {
 | 
					 | 
				
			||||||
			filterBuilder.WriteString(",enabled")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if shortcode != "" {
 | 
					 | 
				
			||||||
		// Specific shortcode only.
 | 
					 | 
				
			||||||
		filterBuilder.WriteString(",shortcode:" + shortcode)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return filterBuilder.String()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// EmojisGet returns an admin view of custom
 | 
					// EmojisGet returns an admin view of custom
 | 
				
			||||||
// emojis, filtered with the given parameters.
 | 
					// emojis, filtered with the given parameters.
 | 
				
			||||||
func (p *Processor) EmojisGet(
 | 
					func (p *Processor) EmojisGet(
 | 
				
			||||||
| 
						 | 
					@ -287,21 +196,24 @@ func (p *Processor) EmojiDelete(
 | 
				
			||||||
// given id, using the provided form parameters.
 | 
					// given id, using the provided form parameters.
 | 
				
			||||||
func (p *Processor) EmojiUpdate(
 | 
					func (p *Processor) EmojiUpdate(
 | 
				
			||||||
	ctx context.Context,
 | 
						ctx context.Context,
 | 
				
			||||||
	id string,
 | 
						emojiID string,
 | 
				
			||||||
	form *apimodel.EmojiUpdateRequest,
 | 
						form *apimodel.EmojiUpdateRequest,
 | 
				
			||||||
) (*apimodel.AdminEmoji, gtserror.WithCode) {
 | 
					) (*apimodel.AdminEmoji, gtserror.WithCode) {
 | 
				
			||||||
	emoji, err := p.state.DB.GetEmojiByID(ctx, id)
 | 
					
 | 
				
			||||||
 | 
						// Get the emoji with given ID from the database.
 | 
				
			||||||
 | 
						emoji, err := p.state.DB.GetEmojiByID(ctx, emojiID)
 | 
				
			||||||
	if err != nil && !errors.Is(err, db.ErrNoEntries) {
 | 
						if err != nil && !errors.Is(err, db.ErrNoEntries) {
 | 
				
			||||||
		err := gtserror.Newf("db error: %w", err)
 | 
							err := gtserror.Newf("error fetching emoji from db: %w", err)
 | 
				
			||||||
		return nil, gtserror.NewErrorInternalError(err)
 | 
							return nil, gtserror.NewErrorInternalError(err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check found.
 | 
				
			||||||
	if emoji == nil {
 | 
						if emoji == nil {
 | 
				
			||||||
		err := gtserror.Newf("no emoji with id %s found in the db", id)
 | 
							const text = "emoji not found"
 | 
				
			||||||
		return nil, gtserror.NewErrorNotFound(err)
 | 
							return nil, gtserror.NewErrorNotFound(errors.New(text), text)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	switch t := form.Type; t {
 | 
						switch form.Type {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	case apimodel.EmojiUpdateCopy:
 | 
						case apimodel.EmojiUpdateCopy:
 | 
				
			||||||
		return p.emojiUpdateCopy(ctx, emoji, form.Shortcode, form.CategoryName)
 | 
							return p.emojiUpdateCopy(ctx, emoji, form.Shortcode, form.CategoryName)
 | 
				
			||||||
| 
						 | 
					@ -313,8 +225,8 @@ func (p *Processor) EmojiUpdate(
 | 
				
			||||||
		return p.emojiUpdateModify(ctx, emoji, form.Image, form.CategoryName)
 | 
							return p.emojiUpdateModify(ctx, emoji, form.Image, form.CategoryName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	default:
 | 
						default:
 | 
				
			||||||
		err := fmt.Errorf("unrecognized emoji action type %s", t)
 | 
							const text = "unrecognized emoji update action type"
 | 
				
			||||||
		return nil, gtserror.NewErrorBadRequest(err, err.Error())
 | 
							return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -342,56 +254,6 @@ func (p *Processor) EmojiCategoriesGet(
 | 
				
			||||||
	return apiCategories, nil
 | 
						return apiCategories, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
	UTIL FUNCTIONS
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// getOrCreateEmojiCategory either gets an existing
 | 
					 | 
				
			||||||
// category with the given name from the database,
 | 
					 | 
				
			||||||
// or, if the category doesn't yet exist, it creates
 | 
					 | 
				
			||||||
// the category and then returns it.
 | 
					 | 
				
			||||||
func (p *Processor) getOrCreateEmojiCategory(
 | 
					 | 
				
			||||||
	ctx context.Context,
 | 
					 | 
				
			||||||
	name string,
 | 
					 | 
				
			||||||
) (*gtsmodel.EmojiCategory, error) {
 | 
					 | 
				
			||||||
	category, err := p.state.DB.GetEmojiCategoryByName(ctx, name)
 | 
					 | 
				
			||||||
	if err != nil && !errors.Is(err, db.ErrNoEntries) {
 | 
					 | 
				
			||||||
		return nil, gtserror.Newf(
 | 
					 | 
				
			||||||
			"database error trying get emoji category %s: %w",
 | 
					 | 
				
			||||||
			name, err,
 | 
					 | 
				
			||||||
		)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if category != nil {
 | 
					 | 
				
			||||||
		// We had it already.
 | 
					 | 
				
			||||||
		return category, nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// We don't have the category yet,
 | 
					 | 
				
			||||||
	// create it with the given name.
 | 
					 | 
				
			||||||
	categoryID, err := id.NewRandomULID()
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil, gtserror.Newf(
 | 
					 | 
				
			||||||
			"error generating id for new emoji category %s: %w",
 | 
					 | 
				
			||||||
			name, err,
 | 
					 | 
				
			||||||
		)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	category = >smodel.EmojiCategory{
 | 
					 | 
				
			||||||
		ID:   categoryID,
 | 
					 | 
				
			||||||
		Name: name,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err := p.state.DB.PutEmojiCategory(ctx, category); err != nil {
 | 
					 | 
				
			||||||
		return nil, gtserror.Newf(
 | 
					 | 
				
			||||||
			"db error putting new emoji category %s: %w",
 | 
					 | 
				
			||||||
			name, err,
 | 
					 | 
				
			||||||
		)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return category, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// emojiUpdateCopy copies and stores the given
 | 
					// emojiUpdateCopy copies and stores the given
 | 
				
			||||||
// *remote* emoji as a *local* emoji, preserving
 | 
					// *remote* emoji as a *local* emoji, preserving
 | 
				
			||||||
// the same image, and using the provided shortcode.
 | 
					// the same image, and using the provided shortcode.
 | 
				
			||||||
| 
						 | 
					@ -400,99 +262,56 @@ func (p *Processor) getOrCreateEmojiCategory(
 | 
				
			||||||
// emoji already stored in the database + storage.
 | 
					// emoji already stored in the database + storage.
 | 
				
			||||||
func (p *Processor) emojiUpdateCopy(
 | 
					func (p *Processor) emojiUpdateCopy(
 | 
				
			||||||
	ctx context.Context,
 | 
						ctx context.Context,
 | 
				
			||||||
	targetEmoji *gtsmodel.Emoji,
 | 
						target *gtsmodel.Emoji,
 | 
				
			||||||
	shortcode *string,
 | 
						shortcode *string,
 | 
				
			||||||
	category *string,
 | 
						categoryName *string,
 | 
				
			||||||
) (*apimodel.AdminEmoji, gtserror.WithCode) {
 | 
					) (*apimodel.AdminEmoji, gtserror.WithCode) {
 | 
				
			||||||
	if targetEmoji.IsLocal() {
 | 
						if target.IsLocal() {
 | 
				
			||||||
		err := fmt.Errorf("emoji %s is not a remote emoji, cannot copy it to local", targetEmoji.ID)
 | 
							const text = "target emoji is not remote; cannot copy to local"
 | 
				
			||||||
		return nil, gtserror.NewErrorBadRequest(err, err.Error())
 | 
							return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if shortcode == nil {
 | 
						// Ensure target emoji is locally cached.
 | 
				
			||||||
		err := errors.New("no shortcode provided")
 | 
						target, err := p.federator.RefreshEmoji(
 | 
				
			||||||
		return nil, gtserror.NewErrorBadRequest(err, err.Error())
 | 
							ctx,
 | 
				
			||||||
	}
 | 
							target,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	sc := *shortcode
 | 
							// no changes we want to make.
 | 
				
			||||||
	if sc == "" {
 | 
							media.AdditionalEmojiInfo{},
 | 
				
			||||||
		err := errors.New("empty shortcode provided")
 | 
							false,
 | 
				
			||||||
		return nil, gtserror.NewErrorBadRequest(err, err.Error())
 | 
						)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							err := gtserror.Newf("error recaching emoji %s: %w", target.ImageRemoteURL, err)
 | 
				
			||||||
 | 
							return nil, gtserror.NewErrorNotFound(err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Ensure we don't already have an emoji
 | 
					 | 
				
			||||||
	// stored locally with this shortcode.
 | 
					 | 
				
			||||||
	maybeExisting, err := p.state.DB.GetEmojiByShortcodeDomain(ctx, sc, "")
 | 
					 | 
				
			||||||
	if err != nil && !errors.Is(err, db.ErrNoEntries) {
 | 
					 | 
				
			||||||
		err := gtserror.Newf("db error checking for emoji with shortcode %s: %w", sc, err)
 | 
					 | 
				
			||||||
		return nil, gtserror.NewErrorInternalError(err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if maybeExisting != nil {
 | 
					 | 
				
			||||||
		err := fmt.Errorf("emoji with shortcode %s already exists on this instance", sc)
 | 
					 | 
				
			||||||
		return nil, gtserror.NewErrorConflict(err, err.Error())
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// We don't have an emoji with this
 | 
					 | 
				
			||||||
	// shortcode yet! Prepare to create it.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Data function for copying just streams media
 | 
						// Data function for copying just streams media
 | 
				
			||||||
	// out of storage into an additional location.
 | 
						// out of storage into an additional location.
 | 
				
			||||||
	//
 | 
						//
 | 
				
			||||||
	// This means that data for the copy persists even
 | 
						// This means that data for the copy persists even
 | 
				
			||||||
	// if the remote copied emoji gets deleted at some point.
 | 
						// if the remote copied emoji gets deleted at some point.
 | 
				
			||||||
	data := func(ctx context.Context) (io.ReadCloser, int64, error) {
 | 
						data := func(ctx context.Context) (io.ReadCloser, int64, error) {
 | 
				
			||||||
		rc, err := p.state.Storage.GetStream(ctx, targetEmoji.ImagePath)
 | 
							rc, err := p.state.Storage.GetStream(ctx, target.ImagePath)
 | 
				
			||||||
		return rc, int64(targetEmoji.ImageFileSize), err
 | 
							return rc, int64(target.ImageFileSize), err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Generate new emoji ID and URI.
 | 
						// Attempt to create the new local emoji.
 | 
				
			||||||
	emojiID, err := id.NewRandomULID()
 | 
						emoji, errWithCode := p.createEmoji(ctx,
 | 
				
			||||||
	if err != nil {
 | 
							util.PtrValueOr(shortcode, ""),
 | 
				
			||||||
		err := gtserror.Newf("error creating id for new emoji: %w", err)
 | 
							util.PtrValueOr(categoryName, ""),
 | 
				
			||||||
		return nil, gtserror.NewErrorInternalError(err)
 | 
							data,
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	emojiURI := uris.URIForEmoji(emojiID)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// If category was supplied, ensure the
 | 
					 | 
				
			||||||
	// category exists and provide it as
 | 
					 | 
				
			||||||
	// additional info to emoji processing.
 | 
					 | 
				
			||||||
	var ai *media.AdditionalEmojiInfo
 | 
					 | 
				
			||||||
	if category != nil && *category != "" {
 | 
					 | 
				
			||||||
		category, err := p.getOrCreateEmojiCategory(ctx, *category)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return nil, gtserror.NewErrorInternalError(err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		ai = &media.AdditionalEmojiInfo{
 | 
					 | 
				
			||||||
			CategoryID: &category.ID,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Begin media processing.
 | 
					 | 
				
			||||||
	processingEmoji, err := p.mediaManager.PreProcessEmoji(ctx,
 | 
					 | 
				
			||||||
		data, sc, emojiID, emojiURI, ai, false,
 | 
					 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
						if errWithCode != nil {
 | 
				
			||||||
 | 
							return nil, errWithCode
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						apiEmoji, err := p.converter.EmojiToAdminAPIEmoji(ctx, emoji)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		err := gtserror.Newf("error processing emoji: %w", err)
 | 
							err := gtserror.Newf("error converting emoji: %w", err)
 | 
				
			||||||
		return nil, gtserror.NewErrorInternalError(err)
 | 
							return nil, gtserror.NewErrorInternalError(err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Complete processing immediately.
 | 
						return apiEmoji, nil
 | 
				
			||||||
	newEmoji, err := processingEmoji.LoadEmoji(ctx)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		err := gtserror.Newf("error loading emoji: %w", err)
 | 
					 | 
				
			||||||
		return nil, gtserror.NewErrorInternalError(err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	adminEmoji, err := p.converter.EmojiToAdminAPIEmoji(ctx, newEmoji)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		err := gtserror.Newf("error converting emoji %s to admin emoji: %w", newEmoji.ID, err)
 | 
					 | 
				
			||||||
		return nil, gtserror.NewErrorInternalError(err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return adminEmoji, nil
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// emojiUpdateDisable marks the given *remote*
 | 
					// emojiUpdateDisable marks the given *remote*
 | 
				
			||||||
| 
						 | 
					@ -521,7 +340,7 @@ func (p *Processor) emojiUpdateDisable(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	adminEmoji, err := p.converter.EmojiToAdminAPIEmoji(ctx, emoji)
 | 
						adminEmoji, err := p.converter.EmojiToAdminAPIEmoji(ctx, emoji)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		err := gtserror.Newf("error converting emoji %s to admin emoji: %w", emoji.ID, err)
 | 
							err := gtserror.Newf("error converting emoji: %w", err)
 | 
				
			||||||
		return nil, gtserror.NewErrorInternalError(err)
 | 
							return nil, gtserror.NewErrorInternalError(err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -541,104 +360,222 @@ func (p *Processor) emojiUpdateModify(
 | 
				
			||||||
	ctx context.Context,
 | 
						ctx context.Context,
 | 
				
			||||||
	emoji *gtsmodel.Emoji,
 | 
						emoji *gtsmodel.Emoji,
 | 
				
			||||||
	image *multipart.FileHeader,
 | 
						image *multipart.FileHeader,
 | 
				
			||||||
	category *string,
 | 
						categoryName *string,
 | 
				
			||||||
) (*apimodel.AdminEmoji, gtserror.WithCode) {
 | 
					) (*apimodel.AdminEmoji, gtserror.WithCode) {
 | 
				
			||||||
	if !emoji.IsLocal() {
 | 
						if !emoji.IsLocal() {
 | 
				
			||||||
		err := fmt.Errorf("emoji %s is not a local emoji, cannot update it via this endpoint", emoji.ID)
 | 
							const text = "cannot modify remote emoji"
 | 
				
			||||||
		return nil, gtserror.NewErrorBadRequest(err, err.Error())
 | 
							return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Ensure there's actually something to update.
 | 
						// Ensure there's actually something to update.
 | 
				
			||||||
	if image == nil && category == nil {
 | 
						if image == nil && categoryName == nil {
 | 
				
			||||||
		err := errors.New("neither new category nor new image set, cannot update")
 | 
							const text = "no changes were provided"
 | 
				
			||||||
		return nil, gtserror.NewErrorBadRequest(err, err.Error())
 | 
							return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Only update category
 | 
						if categoryName != nil {
 | 
				
			||||||
	// if it's changed.
 | 
							if *categoryName != "" {
 | 
				
			||||||
	var (
 | 
								// A category was provided, get / create relevant emoji category.
 | 
				
			||||||
		newCategory      *gtsmodel.EmojiCategory
 | 
								category, errWithCode := p.mustGetEmojiCategory(ctx, *categoryName)
 | 
				
			||||||
		newCategoryID    string
 | 
								if errWithCode != nil {
 | 
				
			||||||
		updateCategoryID bool
 | 
									return nil, errWithCode
 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if category != nil {
 | 
					 | 
				
			||||||
		catName := *category
 | 
					 | 
				
			||||||
		if catName != "" {
 | 
					 | 
				
			||||||
			// Set new category.
 | 
					 | 
				
			||||||
			var err error
 | 
					 | 
				
			||||||
			newCategory, err = p.getOrCreateEmojiCategory(ctx, catName)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				err := gtserror.Newf("error getting or creating category: %w", err)
 | 
					 | 
				
			||||||
				return nil, gtserror.NewErrorInternalError(err)
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			newCategoryID = newCategory.ID
 | 
								if category.ID == emoji.CategoryID {
 | 
				
			||||||
 | 
									// There was no change,
 | 
				
			||||||
 | 
									// indicate this by unsetting
 | 
				
			||||||
 | 
									// the category name pointer.
 | 
				
			||||||
 | 
									categoryName = nil
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
			// Clear existing category.
 | 
									// Update emoji category.
 | 
				
			||||||
			newCategoryID = ""
 | 
									emoji.CategoryID = category.ID
 | 
				
			||||||
 | 
									emoji.Category = category
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								// Emoji category was unset.
 | 
				
			||||||
 | 
								emoji.CategoryID = ""
 | 
				
			||||||
 | 
								emoji.Category = nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		updateCategoryID = emoji.CategoryID != newCategoryID
 | 
						// Check whether any image changes were requested.
 | 
				
			||||||
	}
 | 
						imageUpdated := (image != nil && image.Size > 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Only update image
 | 
						if !imageUpdated && categoryName != nil {
 | 
				
			||||||
	// if one is provided.
 | 
							// Only updating category; only a single database update required.
 | 
				
			||||||
	var updateImage bool
 | 
					 | 
				
			||||||
	if image != nil && image.Size != 0 {
 | 
					 | 
				
			||||||
		updateImage = true
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if updateCategoryID && !updateImage {
 | 
					 | 
				
			||||||
		// Only updating category; we only
 | 
					 | 
				
			||||||
		// need to do a db update for this.
 | 
					 | 
				
			||||||
		emoji.CategoryID = newCategoryID
 | 
					 | 
				
			||||||
		emoji.Category = newCategory
 | 
					 | 
				
			||||||
		if err := p.state.DB.UpdateEmoji(ctx, emoji, "category_id"); err != nil {
 | 
							if err := p.state.DB.UpdateEmoji(ctx, emoji, "category_id"); err != nil {
 | 
				
			||||||
			err := gtserror.Newf("db error updating emoji %s: %w", emoji.ID, err)
 | 
								err := gtserror.Newf("error updating emoji in db: %w", err)
 | 
				
			||||||
			return nil, gtserror.NewErrorInternalError(err)
 | 
								return nil, gtserror.NewErrorInternalError(err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	} else if updateImage {
 | 
						} else if imageUpdated {
 | 
				
			||||||
 | 
							var err error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Updating image and maybe categoryID.
 | 
							// Updating image and maybe categoryID.
 | 
				
			||||||
		// We can do both at the same time :)
 | 
							// We can do both at the same time :)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Set data function to provided image.
 | 
							// Simply read provided form data for emoji data source.
 | 
				
			||||||
		data := func(ctx context.Context) (io.ReadCloser, int64, error) {
 | 
							data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
				
			||||||
			i, err := image.Open()
 | 
								f, err := image.Open()
 | 
				
			||||||
			return i, image.Size, err
 | 
								return f, image.Size, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// If necessary, include
 | 
							// Prepare emoji model for recache from new data.
 | 
				
			||||||
		// update to categoryID too.
 | 
							processing := p.media.RecacheEmoji(emoji, data)
 | 
				
			||||||
		var ai *media.AdditionalEmojiInfo
 | 
					 | 
				
			||||||
		if updateCategoryID {
 | 
					 | 
				
			||||||
			ai = &media.AdditionalEmojiInfo{
 | 
					 | 
				
			||||||
				CategoryID: &newCategoryID,
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Begin media processing.
 | 
							// Load to trigger update + write.
 | 
				
			||||||
		processingEmoji, err := p.mediaManager.PreProcessEmoji(ctx,
 | 
							emoji, err = processing.Load(ctx)
 | 
				
			||||||
			data, emoji.Shortcode, emoji.ID, emoji.URI, ai, false,
 | 
					 | 
				
			||||||
		)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			err := gtserror.Newf("error processing emoji: %w", err)
 | 
								err := gtserror.Newf("error processing emoji %s: %w", emoji.Shortcode, err)
 | 
				
			||||||
			return nil, gtserror.NewErrorInternalError(err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Replace emoji ptr with newly-processed version.
 | 
					 | 
				
			||||||
		emoji, err = processingEmoji.LoadEmoji(ctx)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			err := gtserror.Newf("error loading emoji: %w", err)
 | 
					 | 
				
			||||||
			return nil, gtserror.NewErrorInternalError(err)
 | 
								return nil, gtserror.NewErrorInternalError(err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	adminEmoji, err := p.converter.EmojiToAdminAPIEmoji(ctx, emoji)
 | 
						adminEmoji, err := p.converter.EmojiToAdminAPIEmoji(ctx, emoji)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		err := gtserror.Newf("error converting emoji %s to admin emoji: %w", emoji.ID, err)
 | 
							err := gtserror.Newf("error converting emoji: %w", err)
 | 
				
			||||||
		return nil, gtserror.NewErrorInternalError(err)
 | 
							return nil, gtserror.NewErrorInternalError(err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return adminEmoji, nil
 | 
						return adminEmoji, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// createEmoji will create a new local emoji
 | 
				
			||||||
 | 
					// with the given shortcode, attached category
 | 
				
			||||||
 | 
					// name (if any) and data source function.
 | 
				
			||||||
 | 
					func (p *Processor) createEmoji(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						shortcode string,
 | 
				
			||||||
 | 
						categoryName string,
 | 
				
			||||||
 | 
						data media.DataFunc,
 | 
				
			||||||
 | 
					) (
 | 
				
			||||||
 | 
						*gtsmodel.Emoji,
 | 
				
			||||||
 | 
						gtserror.WithCode,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						// Validate shortcode.
 | 
				
			||||||
 | 
						if shortcode == "" {
 | 
				
			||||||
 | 
							const text = "empty shortcode name"
 | 
				
			||||||
 | 
							return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Look for an existing local emoji with shortcode to ensure this is new.
 | 
				
			||||||
 | 
						existing, err := p.state.DB.GetEmojiByShortcodeDomain(ctx, shortcode, "")
 | 
				
			||||||
 | 
						if err != nil && !errors.Is(err, db.ErrNoEntries) {
 | 
				
			||||||
 | 
							err := gtserror.Newf("error fetching emoji from db: %w", err)
 | 
				
			||||||
 | 
							return nil, gtserror.NewErrorInternalError(err)
 | 
				
			||||||
 | 
						} else if existing != nil {
 | 
				
			||||||
 | 
							const text = "emoji with shortcode already exists"
 | 
				
			||||||
 | 
							return nil, gtserror.NewErrorConflict(errors.New(text), text)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var categoryID *string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if categoryName != "" {
 | 
				
			||||||
 | 
							// A category was provided, get / create relevant emoji category.
 | 
				
			||||||
 | 
							category, errWithCode := p.mustGetEmojiCategory(ctx, categoryName)
 | 
				
			||||||
 | 
							if errWithCode != nil {
 | 
				
			||||||
 | 
								return nil, errWithCode
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Set category ID for emoji.
 | 
				
			||||||
 | 
							categoryID = &category.ID
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Store to instance storage.
 | 
				
			||||||
 | 
						return p.c.StoreLocalEmoji(
 | 
				
			||||||
 | 
							ctx,
 | 
				
			||||||
 | 
							shortcode,
 | 
				
			||||||
 | 
							data,
 | 
				
			||||||
 | 
							media.AdditionalEmojiInfo{
 | 
				
			||||||
 | 
								CategoryID: categoryID,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// mustGetEmojiCategory either gets an existing
 | 
				
			||||||
 | 
					// category with the given name from the database,
 | 
				
			||||||
 | 
					// or, if the category doesn't yet exist, it creates
 | 
				
			||||||
 | 
					// the category and then returns it.
 | 
				
			||||||
 | 
					func (p *Processor) mustGetEmojiCategory(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						name string,
 | 
				
			||||||
 | 
					) (
 | 
				
			||||||
 | 
						*gtsmodel.EmojiCategory,
 | 
				
			||||||
 | 
						gtserror.WithCode,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						// Look for an existing emoji category with name.
 | 
				
			||||||
 | 
						category, err := p.state.DB.GetEmojiCategoryByName(ctx, name)
 | 
				
			||||||
 | 
						if err != nil && !errors.Is(err, db.ErrNoEntries) {
 | 
				
			||||||
 | 
							err := gtserror.Newf("error fetching emoji category from db: %w", err)
 | 
				
			||||||
 | 
							return nil, gtserror.NewErrorInternalError(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if category != nil {
 | 
				
			||||||
 | 
							// We had it already.
 | 
				
			||||||
 | 
							return category, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create new ID.
 | 
				
			||||||
 | 
						id := id.NewULID()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Prepare new category for insertion.
 | 
				
			||||||
 | 
						category = >smodel.EmojiCategory{
 | 
				
			||||||
 | 
							ID:   id,
 | 
				
			||||||
 | 
							Name: name,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Insert new category into the database.
 | 
				
			||||||
 | 
						err = p.state.DB.PutEmojiCategory(ctx, category)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							err := gtserror.Newf("error inserting emoji category into db: %w", err)
 | 
				
			||||||
 | 
							return nil, gtserror.NewErrorInternalError(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return category, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// emojisGetFilterParams builds extra
 | 
				
			||||||
 | 
					// query parameters to return as part
 | 
				
			||||||
 | 
					// of an Emojis pageable response.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// The returned string will look like:
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// "filter=domain:all,enabled,shortcode:example"
 | 
				
			||||||
 | 
					func emojisGetFilterParams(
 | 
				
			||||||
 | 
						shortcode string,
 | 
				
			||||||
 | 
						domain string,
 | 
				
			||||||
 | 
						includeDisabled bool,
 | 
				
			||||||
 | 
						includeEnabled bool,
 | 
				
			||||||
 | 
					) string {
 | 
				
			||||||
 | 
						var filterBuilder strings.Builder
 | 
				
			||||||
 | 
						filterBuilder.WriteString("filter=")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch domain {
 | 
				
			||||||
 | 
						case "", "local":
 | 
				
			||||||
 | 
							// Local emojis only.
 | 
				
			||||||
 | 
							filterBuilder.WriteString("domain:local")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						case db.EmojiAllDomains:
 | 
				
			||||||
 | 
							// Local or remote.
 | 
				
			||||||
 | 
							filterBuilder.WriteString("domain:all")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							// Specific domain only.
 | 
				
			||||||
 | 
							filterBuilder.WriteString("domain:" + domain)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if includeDisabled != includeEnabled {
 | 
				
			||||||
 | 
							if includeDisabled {
 | 
				
			||||||
 | 
								filterBuilder.WriteString(",disabled")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if includeEnabled {
 | 
				
			||||||
 | 
								filterBuilder.WriteString(",enabled")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if shortcode != "" {
 | 
				
			||||||
 | 
							// Specific shortcode only.
 | 
				
			||||||
 | 
							filterBuilder.WriteString(",shortcode:" + shortcode)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return filterBuilder.String()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,7 +28,7 @@ import (
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// MediaRefetch forces a refetch of remote emojis.
 | 
					// MediaRefetch forces a refetch of remote emojis.
 | 
				
			||||||
func (p *Processor) MediaRefetch(ctx context.Context, requestingAccount *gtsmodel.Account, domain string) gtserror.WithCode {
 | 
					func (p *Processor) MediaRefetch(ctx context.Context, requestingAccount *gtsmodel.Account, domain string) gtserror.WithCode {
 | 
				
			||||||
	transport, err := p.transportController.NewTransportForUsername(ctx, requestingAccount.Username)
 | 
						transport, err := p.transport.NewTransportForUsername(ctx, requestingAccount.Username)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		err = fmt.Errorf("error getting transport for user %s during media refetch request: %w", requestingAccount.Username, err)
 | 
							err = fmt.Errorf("error getting transport for user %s during media refetch request: %w", requestingAccount.Username, err)
 | 
				
			||||||
		return gtserror.NewErrorInternalError(err)
 | 
							return gtserror.NewErrorInternalError(err)
 | 
				
			||||||
| 
						 | 
					@ -36,7 +36,7 @@ func (p *Processor) MediaRefetch(ctx context.Context, requestingAccount *gtsmode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	go func() {
 | 
						go func() {
 | 
				
			||||||
		log.Info(ctx, "starting emoji refetch")
 | 
							log.Info(ctx, "starting emoji refetch")
 | 
				
			||||||
		refetched, err := p.mediaManager.RefetchEmojis(context.Background(), domain, transport.DereferenceMedia)
 | 
							refetched, err := p.media.RefetchEmojis(context.Background(), domain, transport.DereferenceMedia)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			log.Errorf(ctx, "error refetching emojis: %s", err)
 | 
								log.Errorf(ctx, "error refetching emojis: %s", err)
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,6 +20,7 @@ package common
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/federation"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/federation"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
 | 
				
			||||||
 | 
						"github.com/superseriousbusiness/gotosocial/internal/media"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/state"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/state"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/typeutils"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/typeutils"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
| 
						 | 
					@ -29,6 +30,7 @@ import (
 | 
				
			||||||
// processing subsection of the codebase.
 | 
					// processing subsection of the codebase.
 | 
				
			||||||
type Processor struct {
 | 
					type Processor struct {
 | 
				
			||||||
	state     *state.State
 | 
						state     *state.State
 | 
				
			||||||
 | 
						media     *media.Manager
 | 
				
			||||||
	converter *typeutils.Converter
 | 
						converter *typeutils.Converter
 | 
				
			||||||
	federator *federation.Federator
 | 
						federator *federation.Federator
 | 
				
			||||||
	filter    *visibility.Filter
 | 
						filter    *visibility.Filter
 | 
				
			||||||
| 
						 | 
					@ -37,12 +39,14 @@ type Processor struct {
 | 
				
			||||||
// New returns a new Processor instance.
 | 
					// New returns a new Processor instance.
 | 
				
			||||||
func New(
 | 
					func New(
 | 
				
			||||||
	state *state.State,
 | 
						state *state.State,
 | 
				
			||||||
 | 
						media *media.Manager,
 | 
				
			||||||
	converter *typeutils.Converter,
 | 
						converter *typeutils.Converter,
 | 
				
			||||||
	federator *federation.Federator,
 | 
						federator *federation.Federator,
 | 
				
			||||||
	filter *visibility.Filter,
 | 
						filter *visibility.Filter,
 | 
				
			||||||
) Processor {
 | 
					) Processor {
 | 
				
			||||||
	return Processor{
 | 
						return Processor{
 | 
				
			||||||
		state:     state,
 | 
							state:     state,
 | 
				
			||||||
 | 
							media:     media,
 | 
				
			||||||
		converter: converter,
 | 
							converter: converter,
 | 
				
			||||||
		federator: federator,
 | 
							federator: federator,
 | 
				
			||||||
		filter:    filter,
 | 
							filter:    filter,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										98
									
								
								internal/processing/common/media.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								internal/processing/common/media.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,98 @@
 | 
				
			||||||
 | 
					// GoToSocial
 | 
				
			||||||
 | 
					// Copyright (C) GoToSocial Authors admin@gotosocial.org
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: AGPL-3.0-or-later
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// 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 common
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/superseriousbusiness/gotosocial/internal/gtserror"
 | 
				
			||||||
 | 
						"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 | 
				
			||||||
 | 
						"github.com/superseriousbusiness/gotosocial/internal/media"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// StoreLocalMedia is a wrapper around CreateMedia() and
 | 
				
			||||||
 | 
					// ProcessingMedia{}.Load() with appropriate error responses.
 | 
				
			||||||
 | 
					func (p *Processor) StoreLocalMedia(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						accountID string,
 | 
				
			||||||
 | 
						data media.DataFunc,
 | 
				
			||||||
 | 
						info media.AdditionalMediaInfo,
 | 
				
			||||||
 | 
					) (
 | 
				
			||||||
 | 
						*gtsmodel.MediaAttachment,
 | 
				
			||||||
 | 
						gtserror.WithCode,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						// Create a new processing media attachment.
 | 
				
			||||||
 | 
						processing, err := p.media.CreateMedia(ctx,
 | 
				
			||||||
 | 
							accountID,
 | 
				
			||||||
 | 
							data,
 | 
				
			||||||
 | 
							info,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							err := gtserror.Newf("error creating media: %w", err)
 | 
				
			||||||
 | 
							return nil, gtserror.NewErrorInternalError(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Immediately trigger write to storage.
 | 
				
			||||||
 | 
						attachment, err := processing.Load(ctx)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							const text = "error processing emoji"
 | 
				
			||||||
 | 
							err := gtserror.Newf("error processing media: %w", err)
 | 
				
			||||||
 | 
							return nil, gtserror.NewErrorUnprocessableEntity(err, text)
 | 
				
			||||||
 | 
						} else if attachment.Type == gtsmodel.FileTypeUnknown {
 | 
				
			||||||
 | 
							text := fmt.Sprintf("could not process %s type media", attachment.File.ContentType)
 | 
				
			||||||
 | 
							return nil, gtserror.NewErrorUnprocessableEntity(errors.New(text), text)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return attachment, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// StoreLocalMedia is a wrapper around CreateMedia() and
 | 
				
			||||||
 | 
					// ProcessingMedia{}.Load() with appropriate error responses.
 | 
				
			||||||
 | 
					func (p *Processor) StoreLocalEmoji(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						shortcode string,
 | 
				
			||||||
 | 
						data media.DataFunc,
 | 
				
			||||||
 | 
						info media.AdditionalEmojiInfo,
 | 
				
			||||||
 | 
					) (
 | 
				
			||||||
 | 
						*gtsmodel.Emoji,
 | 
				
			||||||
 | 
						gtserror.WithCode,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						// Create a new processing emoji media.
 | 
				
			||||||
 | 
						processing, err := p.media.CreateEmoji(ctx,
 | 
				
			||||||
 | 
							shortcode,
 | 
				
			||||||
 | 
							"", // domain = "" -> local
 | 
				
			||||||
 | 
							data,
 | 
				
			||||||
 | 
							info,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							err := gtserror.Newf("error creating emoji: %w", err)
 | 
				
			||||||
 | 
							return nil, gtserror.NewErrorInternalError(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Immediately write to storage.
 | 
				
			||||||
 | 
						emoji, err := processing.Load(ctx)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							const text = "error processing emoji"
 | 
				
			||||||
 | 
							err := gtserror.Newf("error processing emoji %s: %w", shortcode, err)
 | 
				
			||||||
 | 
							return nil, gtserror.NewErrorUnprocessableEntity(err, text)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return emoji, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -246,9 +246,13 @@ func (p *Processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSe
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if form.Avatar != nil && form.Avatar.Size != 0 {
 | 
						if form.Avatar != nil && form.Avatar.Size != 0 {
 | 
				
			||||||
		// Process instance avatar image + description.
 | 
							// Process instance avatar image + description.
 | 
				
			||||||
		avatarInfo, err := p.account.UpdateAvatar(ctx, form.Avatar, form.AvatarDescription, instanceAcc.ID)
 | 
							avatarInfo, errWithCode := p.account.UpdateAvatar(ctx,
 | 
				
			||||||
		if err != nil {
 | 
								instanceAcc,
 | 
				
			||||||
			return nil, gtserror.NewErrorBadRequest(err, "error processing avatar")
 | 
								form.Avatar,
 | 
				
			||||||
 | 
								form.AvatarDescription,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							if errWithCode != nil {
 | 
				
			||||||
 | 
								return nil, errWithCode
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		instanceAcc.AvatarMediaAttachmentID = avatarInfo.ID
 | 
							instanceAcc.AvatarMediaAttachmentID = avatarInfo.ID
 | 
				
			||||||
		instanceAcc.AvatarMediaAttachment = avatarInfo
 | 
							instanceAcc.AvatarMediaAttachment = avatarInfo
 | 
				
			||||||
| 
						 | 
					@ -264,9 +268,13 @@ func (p *Processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSe
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if form.Header != nil && form.Header.Size != 0 {
 | 
						if form.Header != nil && form.Header.Size != 0 {
 | 
				
			||||||
		// process instance header image
 | 
							// process instance header image
 | 
				
			||||||
		headerInfo, err := p.account.UpdateHeader(ctx, form.Header, nil, instanceAcc.ID)
 | 
							headerInfo, errWithCode := p.account.UpdateHeader(ctx,
 | 
				
			||||||
		if err != nil {
 | 
								instanceAcc,
 | 
				
			||||||
			return nil, gtserror.NewErrorBadRequest(err, "error processing header")
 | 
								form.Header,
 | 
				
			||||||
 | 
								nil,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							if errWithCode != nil {
 | 
				
			||||||
 | 
								return nil, errWithCode
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		instanceAcc.HeaderMediaAttachmentID = headerInfo.ID
 | 
							instanceAcc.HeaderMediaAttachmentID = headerInfo.ID
 | 
				
			||||||
		instanceAcc.HeaderMediaAttachment = headerInfo
 | 
							instanceAcc.HeaderMediaAttachment = headerInfo
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -30,7 +30,7 @@ import (
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Create creates a new media attachment belonging to the given account, using the request form.
 | 
					// Create creates a new media attachment belonging to the given account, using the request form.
 | 
				
			||||||
func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, gtserror.WithCode) {
 | 
					func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, gtserror.WithCode) {
 | 
				
			||||||
	data := func(innerCtx context.Context) (io.ReadCloser, int64, error) {
 | 
						data := func(_ context.Context) (io.ReadCloser, int64, error) {
 | 
				
			||||||
		f, err := form.File.Open()
 | 
							f, err := form.File.Open()
 | 
				
			||||||
		return f, form.File.Size, err
 | 
							return f, form.File.Size, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -41,19 +41,18 @@ func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form
 | 
				
			||||||
		return nil, gtserror.NewErrorBadRequest(err, err.Error())
 | 
							return nil, gtserror.NewErrorBadRequest(err, err.Error())
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// process the media attachment and load it immediately
 | 
						// Create local media and write to instance storage.
 | 
				
			||||||
	media := p.mediaManager.PreProcessMedia(data, account.ID, &media.AdditionalMediaInfo{
 | 
						attachment, errWithCode := p.c.StoreLocalMedia(ctx,
 | 
				
			||||||
 | 
							account.ID,
 | 
				
			||||||
 | 
							data,
 | 
				
			||||||
 | 
							media.AdditionalMediaInfo{
 | 
				
			||||||
			Description: &form.Description,
 | 
								Description: &form.Description,
 | 
				
			||||||
			FocusX:      &focusX,
 | 
								FocusX:      &focusX,
 | 
				
			||||||
			FocusY:      &focusY,
 | 
								FocusY:      &focusY,
 | 
				
			||||||
	})
 | 
							},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	attachment, err := media.LoadAttachment(ctx)
 | 
						if errWithCode != nil {
 | 
				
			||||||
	if err != nil {
 | 
							return nil, errWithCode
 | 
				
			||||||
		return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
 | 
					 | 
				
			||||||
	} else if attachment.Type == gtsmodel.FileTypeUnknown {
 | 
					 | 
				
			||||||
		err = gtserror.Newf("could not process uploaded file with extension %s", attachment.File.ContentType)
 | 
					 | 
				
			||||||
		return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	apiAttachment, err := p.converter.AttachmentToAPIAttachment(ctx, attachment)
 | 
						apiAttachment, err := p.converter.AttachmentToAPIAttachment(ctx, attachment)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,14 +19,14 @@ package media
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io"
 | 
					 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
 | 
						apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/db"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/gtserror"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/media"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/media"
 | 
				
			||||||
| 
						 | 
					@ -38,7 +38,7 @@ import (
 | 
				
			||||||
// to the caller via an io.reader embedded in *apimodel.Content.
 | 
					// to the caller via an io.reader embedded in *apimodel.Content.
 | 
				
			||||||
func (p *Processor) GetFile(
 | 
					func (p *Processor) GetFile(
 | 
				
			||||||
	ctx context.Context,
 | 
						ctx context.Context,
 | 
				
			||||||
	requestingAccount *gtsmodel.Account,
 | 
						requester *gtsmodel.Account,
 | 
				
			||||||
	form *apimodel.GetContentRequestForm,
 | 
						form *apimodel.GetContentRequestForm,
 | 
				
			||||||
) (*apimodel.Content, gtserror.WithCode) {
 | 
					) (*apimodel.Content, gtserror.WithCode) {
 | 
				
			||||||
	// parse the form fields
 | 
						// parse the form fields
 | 
				
			||||||
| 
						 | 
					@ -69,13 +69,13 @@ func (p *Processor) GetFile(
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// make sure the requesting account and the media account don't block each other
 | 
						// make sure the requesting account and the media account don't block each other
 | 
				
			||||||
	if requestingAccount != nil {
 | 
						if requester != nil {
 | 
				
			||||||
		blocked, err := p.state.DB.IsEitherBlocked(ctx, requestingAccount.ID, owningAccountID)
 | 
							blocked, err := p.state.DB.IsEitherBlocked(ctx, requester.ID, owningAccountID)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return nil, gtserror.NewErrorNotFound(fmt.Errorf("block status could not be established between accounts %s and %s: %s", owningAccountID, requestingAccount.ID, err))
 | 
								return nil, gtserror.NewErrorNotFound(fmt.Errorf("block status could not be established between accounts %s and %s: %s", owningAccountID, requester.ID, err))
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if blocked {
 | 
							if blocked {
 | 
				
			||||||
			return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts %s and %s", owningAccountID, requestingAccount.ID))
 | 
								return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts %s and %s", owningAccountID, requester.ID))
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -83,17 +83,254 @@ func (p *Processor) GetFile(
 | 
				
			||||||
	// so we need to take different steps depending on the media type being requested
 | 
						// so we need to take different steps depending on the media type being requested
 | 
				
			||||||
	switch mediaType {
 | 
						switch mediaType {
 | 
				
			||||||
	case media.TypeEmoji:
 | 
						case media.TypeEmoji:
 | 
				
			||||||
		return p.getEmojiContent(ctx, wantedMediaID, owningAccountID, mediaSize)
 | 
							return p.getEmojiContent(ctx,
 | 
				
			||||||
 | 
								owningAccountID,
 | 
				
			||||||
 | 
								wantedMediaID,
 | 
				
			||||||
 | 
								mediaSize,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
	case media.TypeAttachment, media.TypeHeader, media.TypeAvatar:
 | 
						case media.TypeAttachment, media.TypeHeader, media.TypeAvatar:
 | 
				
			||||||
		return p.getAttachmentContent(ctx, requestingAccount, wantedMediaID, owningAccountID, mediaSize)
 | 
							return p.getAttachmentContent(ctx,
 | 
				
			||||||
 | 
								requester,
 | 
				
			||||||
 | 
								owningAccountID,
 | 
				
			||||||
 | 
								wantedMediaID,
 | 
				
			||||||
 | 
								mediaSize,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
	default:
 | 
						default:
 | 
				
			||||||
		return nil, gtserror.NewErrorNotFound(fmt.Errorf("media type %s not recognized", mediaType))
 | 
							return nil, gtserror.NewErrorNotFound(fmt.Errorf("media type %s not recognized", mediaType))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/*
 | 
					func (p *Processor) getAttachmentContent(
 | 
				
			||||||
	UTIL FUNCTIONS
 | 
						ctx context.Context,
 | 
				
			||||||
*/
 | 
						requester *gtsmodel.Account,
 | 
				
			||||||
 | 
						ownerID string,
 | 
				
			||||||
 | 
						mediaID string,
 | 
				
			||||||
 | 
						sizeStr media.Size,
 | 
				
			||||||
 | 
					) (
 | 
				
			||||||
 | 
						*apimodel.Content,
 | 
				
			||||||
 | 
						gtserror.WithCode,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						// Search for media with given ID in the database.
 | 
				
			||||||
 | 
						attach, err := p.state.DB.GetAttachmentByID(ctx, mediaID)
 | 
				
			||||||
 | 
						if err != nil && !errors.Is(err, db.ErrNoEntries) {
 | 
				
			||||||
 | 
							err := gtserror.Newf("error fetching media from database: %w", err)
 | 
				
			||||||
 | 
							return nil, gtserror.NewErrorInternalError(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if attach == nil {
 | 
				
			||||||
 | 
							const text = "media not found"
 | 
				
			||||||
 | 
							return nil, gtserror.NewErrorNotFound(errors.New(text), text)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ensure the 'owner' owns media.
 | 
				
			||||||
 | 
						if attach.AccountID != ownerID {
 | 
				
			||||||
 | 
							const text = "media was not owned by passed account id"
 | 
				
			||||||
 | 
							return nil, gtserror.NewErrorNotFound(errors.New(text) /* no help text! */)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var remoteURL *url.URL
 | 
				
			||||||
 | 
						if attach.RemoteURL != "" {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Parse media remote URL to valid URL object.
 | 
				
			||||||
 | 
							remoteURL, err = url.Parse(attach.RemoteURL)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								err := gtserror.Newf("invalid media remote url %s: %w", attach.RemoteURL, err)
 | 
				
			||||||
 | 
								return nil, gtserror.NewErrorInternalError(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Uknown file types indicate no *locally*
 | 
				
			||||||
 | 
						// stored data we can serve. Handle separately.
 | 
				
			||||||
 | 
						if attach.Type == gtsmodel.FileTypeUnknown {
 | 
				
			||||||
 | 
							if remoteURL == nil {
 | 
				
			||||||
 | 
								err := gtserror.Newf("missing remote url for unknown type media %s: %w", attach.ID, err)
 | 
				
			||||||
 | 
								return nil, gtserror.NewErrorInternalError(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// If this is an "Unknown" file type, ie., one we
 | 
				
			||||||
 | 
							// tried to process and couldn't, or one we refused
 | 
				
			||||||
 | 
							// to process because it wasn't supported, then we
 | 
				
			||||||
 | 
							// can skip a lot of steps here by simply forwarding
 | 
				
			||||||
 | 
							// the request to the remote URL.
 | 
				
			||||||
 | 
							url := &storage.PresignedURL{
 | 
				
			||||||
 | 
								URL: remoteURL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// We might manage to cache the media
 | 
				
			||||||
 | 
								// at some point, so set a low-ish expiry.
 | 
				
			||||||
 | 
								Expiry: time.Now().Add(2 * time.Hour),
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return &apimodel.Content{URL: url}, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var requestUser string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if requester != nil {
 | 
				
			||||||
 | 
							// Set requesting acc username.
 | 
				
			||||||
 | 
							requestUser = requester.Username
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ensure that stored media is cached.
 | 
				
			||||||
 | 
						// (this handles local media / recaches).
 | 
				
			||||||
 | 
						attach, err = p.federator.RefreshMedia(
 | 
				
			||||||
 | 
							ctx,
 | 
				
			||||||
 | 
							requestUser,
 | 
				
			||||||
 | 
							attach,
 | 
				
			||||||
 | 
							media.AdditionalMediaInfo{},
 | 
				
			||||||
 | 
							false,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							err := gtserror.Newf("error recaching media: %w", err)
 | 
				
			||||||
 | 
							return nil, gtserror.NewErrorNotFound(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Start preparing API content model.
 | 
				
			||||||
 | 
						apiContent := &apimodel.Content{
 | 
				
			||||||
 | 
							ContentUpdated: attach.UpdatedAt,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Retrieve appropriate
 | 
				
			||||||
 | 
						// size file from storage.
 | 
				
			||||||
 | 
						switch sizeStr {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						case media.SizeOriginal:
 | 
				
			||||||
 | 
							apiContent.ContentType = attach.File.ContentType
 | 
				
			||||||
 | 
							apiContent.ContentLength = int64(attach.File.FileSize)
 | 
				
			||||||
 | 
							return p.getContent(ctx,
 | 
				
			||||||
 | 
								attach.File.Path,
 | 
				
			||||||
 | 
								apiContent,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						case media.SizeSmall:
 | 
				
			||||||
 | 
							apiContent.ContentType = attach.Thumbnail.ContentType
 | 
				
			||||||
 | 
							apiContent.ContentLength = int64(attach.Thumbnail.FileSize)
 | 
				
			||||||
 | 
							return p.getContent(ctx,
 | 
				
			||||||
 | 
								attach.Thumbnail.Path,
 | 
				
			||||||
 | 
								apiContent,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							const text = "invalid media attachment size"
 | 
				
			||||||
 | 
							return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (p *Processor) getEmojiContent(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ownerID string,
 | 
				
			||||||
 | 
						emojiID string,
 | 
				
			||||||
 | 
						sizeStr media.Size,
 | 
				
			||||||
 | 
					) (
 | 
				
			||||||
 | 
						*apimodel.Content,
 | 
				
			||||||
 | 
						gtserror.WithCode,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						// Reconstruct static emoji image URL to search for it.
 | 
				
			||||||
 | 
						// As refreshed emojis use a newly generated path ID to
 | 
				
			||||||
 | 
						// differentiate them (cache-wise) from the original.
 | 
				
			||||||
 | 
						staticURL := uris.URIForAttachment(
 | 
				
			||||||
 | 
							ownerID,
 | 
				
			||||||
 | 
							string(media.TypeEmoji),
 | 
				
			||||||
 | 
							string(media.SizeStatic),
 | 
				
			||||||
 | 
							emojiID,
 | 
				
			||||||
 | 
							"png",
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Search for emoji with given static URL in the database.
 | 
				
			||||||
 | 
						emoji, err := p.state.DB.GetEmojiByStaticURL(ctx, staticURL)
 | 
				
			||||||
 | 
						if err != nil && !errors.Is(err, db.ErrNoEntries) {
 | 
				
			||||||
 | 
							err := gtserror.Newf("error fetching emoji from database: %w", err)
 | 
				
			||||||
 | 
							return nil, gtserror.NewErrorInternalError(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if emoji == nil {
 | 
				
			||||||
 | 
							const text = "emoji not found"
 | 
				
			||||||
 | 
							return nil, gtserror.NewErrorNotFound(errors.New(text), text)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if *emoji.Disabled {
 | 
				
			||||||
 | 
							const text = "emoji has been disabled"
 | 
				
			||||||
 | 
							return nil, gtserror.NewErrorNotFound(errors.New(text), text)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ensure that stored emoji is cached.
 | 
				
			||||||
 | 
						// (this handles local emoji / recaches).
 | 
				
			||||||
 | 
						emoji, err = p.federator.RefreshEmoji(
 | 
				
			||||||
 | 
							ctx,
 | 
				
			||||||
 | 
							emoji,
 | 
				
			||||||
 | 
							media.AdditionalEmojiInfo{},
 | 
				
			||||||
 | 
							false,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							err := gtserror.Newf("error recaching emoji: %w", err)
 | 
				
			||||||
 | 
							return nil, gtserror.NewErrorNotFound(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Start preparing API content model.
 | 
				
			||||||
 | 
						apiContent := &apimodel.Content{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Retrieve appropriate
 | 
				
			||||||
 | 
						// size file from storage.
 | 
				
			||||||
 | 
						switch sizeStr {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						case media.SizeOriginal:
 | 
				
			||||||
 | 
							apiContent.ContentType = emoji.ImageContentType
 | 
				
			||||||
 | 
							apiContent.ContentLength = int64(emoji.ImageFileSize)
 | 
				
			||||||
 | 
							return p.getContent(ctx,
 | 
				
			||||||
 | 
								emoji.ImagePath,
 | 
				
			||||||
 | 
								apiContent,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						case media.SizeStatic:
 | 
				
			||||||
 | 
							apiContent.ContentType = emoji.ImageStaticContentType
 | 
				
			||||||
 | 
							apiContent.ContentLength = int64(emoji.ImageStaticFileSize)
 | 
				
			||||||
 | 
							return p.getContent(ctx,
 | 
				
			||||||
 | 
								emoji.ImageStaticPath,
 | 
				
			||||||
 | 
								apiContent,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							const text = "invalid media attachment size"
 | 
				
			||||||
 | 
							return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// getContent performs the final file fetching of
 | 
				
			||||||
 | 
					// stored content at path in storage. This is
 | 
				
			||||||
 | 
					// populated in the apimodel.Content{} and returned.
 | 
				
			||||||
 | 
					// (note: this also handles un-proxied S3 storage).
 | 
				
			||||||
 | 
					func (p *Processor) getContent(
 | 
				
			||||||
 | 
						ctx context.Context,
 | 
				
			||||||
 | 
						path string,
 | 
				
			||||||
 | 
						content *apimodel.Content,
 | 
				
			||||||
 | 
					) (
 | 
				
			||||||
 | 
						*apimodel.Content,
 | 
				
			||||||
 | 
						gtserror.WithCode,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						// If running on S3 storage with proxying disabled then
 | 
				
			||||||
 | 
						// just fetch pre-signed URL instead of the content.
 | 
				
			||||||
 | 
						if url := p.state.Storage.URL(ctx, path); url != nil {
 | 
				
			||||||
 | 
							content.URL = url
 | 
				
			||||||
 | 
							return content, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Fetch file stream for the stored media at path.
 | 
				
			||||||
 | 
						rc, err := p.state.Storage.GetStream(ctx, path)
 | 
				
			||||||
 | 
						if err != nil && !storage.IsNotFound(err) {
 | 
				
			||||||
 | 
							err := gtserror.Newf("error getting file %s from storage: %w", path, err)
 | 
				
			||||||
 | 
							return nil, gtserror.NewErrorInternalError(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ensure found.
 | 
				
			||||||
 | 
						if rc == nil {
 | 
				
			||||||
 | 
							const text = "file not found"
 | 
				
			||||||
 | 
							return nil, gtserror.NewErrorNotFound(errors.New(text), text)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Return with stream.
 | 
				
			||||||
 | 
						content.Content = rc
 | 
				
			||||||
 | 
						return content, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func parseType(s string) (media.Type, error) {
 | 
					func parseType(s string) (media.Type, error) {
 | 
				
			||||||
	switch s {
 | 
						switch s {
 | 
				
			||||||
| 
						 | 
					@ -120,198 +357,3 @@ func parseSize(s string) (media.Size, error) {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return "", fmt.Errorf("%s not a recognized media.Size", s)
 | 
						return "", fmt.Errorf("%s not a recognized media.Size", s)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
func (p *Processor) getAttachmentContent(ctx context.Context, requestingAccount *gtsmodel.Account, wantedMediaID string, owningAccountID string, mediaSize media.Size) (*apimodel.Content, gtserror.WithCode) {
 | 
					 | 
				
			||||||
	// retrieve attachment from the database and do basic checks on it
 | 
					 | 
				
			||||||
	a, err := p.state.DB.GetAttachmentByID(ctx, wantedMediaID)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		err = gtserror.Newf("attachment %s could not be taken from the db: %w", wantedMediaID, err)
 | 
					 | 
				
			||||||
		return nil, gtserror.NewErrorNotFound(err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if a.AccountID != owningAccountID {
 | 
					 | 
				
			||||||
		err = gtserror.Newf("attachment %s is not owned by %s", wantedMediaID, owningAccountID)
 | 
					 | 
				
			||||||
		return nil, gtserror.NewErrorNotFound(err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// If this is an "Unknown" file type, ie., one we
 | 
					 | 
				
			||||||
	// tried to process and couldn't, or one we refused
 | 
					 | 
				
			||||||
	// to process because it wasn't supported, then we
 | 
					 | 
				
			||||||
	// can skip a lot of steps here by simply forwarding
 | 
					 | 
				
			||||||
	// the request to the remote URL.
 | 
					 | 
				
			||||||
	if a.Type == gtsmodel.FileTypeUnknown {
 | 
					 | 
				
			||||||
		remoteURL, err := url.Parse(a.RemoteURL)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			err = gtserror.Newf("error parsing remote URL of 'Unknown'-type attachment for redirection: %w", err)
 | 
					 | 
				
			||||||
			return nil, gtserror.NewErrorInternalError(err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		url := &storage.PresignedURL{
 | 
					 | 
				
			||||||
			URL: remoteURL,
 | 
					 | 
				
			||||||
			// We might manage to cache the media
 | 
					 | 
				
			||||||
			// at some point, so set a low-ish expiry.
 | 
					 | 
				
			||||||
			Expiry: time.Now().Add(2 * time.Hour),
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return &apimodel.Content{URL: url}, nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if !*a.Cached {
 | 
					 | 
				
			||||||
		// if we don't have it cached, then we can assume two things:
 | 
					 | 
				
			||||||
		// 1. this is remote media, since local media should never be uncached
 | 
					 | 
				
			||||||
		// 2. we need to fetch it again using a transport and the media manager
 | 
					 | 
				
			||||||
		remoteMediaIRI, err := url.Parse(a.RemoteURL)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return nil, gtserror.NewErrorNotFound(fmt.Errorf("error parsing remote media iri %s: %w", a.RemoteURL, err))
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// use an empty string as requestingUsername to use the instance account, unless the request for this
 | 
					 | 
				
			||||||
		// media has been http signed, then use the requesting account to make the request to remote server
 | 
					 | 
				
			||||||
		var requestingUsername string
 | 
					 | 
				
			||||||
		if requestingAccount != nil {
 | 
					 | 
				
			||||||
			requestingUsername = requestingAccount.Username
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Pour one out for tobi's original streamed recache
 | 
					 | 
				
			||||||
		// (streaming data both to the client and storage).
 | 
					 | 
				
			||||||
		// Gone and forever missed <3
 | 
					 | 
				
			||||||
		//
 | 
					 | 
				
			||||||
		// [
 | 
					 | 
				
			||||||
		//   the reason it was removed was because a slow
 | 
					 | 
				
			||||||
		//   client connection could hold open a storage
 | 
					 | 
				
			||||||
		//   recache operation -> holding open a media worker.
 | 
					 | 
				
			||||||
		// ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		dataFn := func(ctx context.Context) (io.ReadCloser, int64, error) {
 | 
					 | 
				
			||||||
			t, err := p.transportController.NewTransportForUsername(ctx, requestingUsername)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return nil, 0, err
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			return t.DereferenceMedia(gtscontext.SetFastFail(ctx), remoteMediaIRI)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Start recaching this media with the prepared data function.
 | 
					 | 
				
			||||||
		processingMedia, err := p.mediaManager.PreProcessMediaRecache(ctx, dataFn, wantedMediaID)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return nil, gtserror.NewErrorNotFound(fmt.Errorf("error recaching media: %w", err))
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Load attachment and block until complete
 | 
					 | 
				
			||||||
		a, err = processingMedia.LoadAttachment(ctx)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return nil, gtserror.NewErrorNotFound(fmt.Errorf("error loading recached attachment: %w", err))
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var (
 | 
					 | 
				
			||||||
		storagePath       string
 | 
					 | 
				
			||||||
		attachmentContent = &apimodel.Content{
 | 
					 | 
				
			||||||
			ContentUpdated: a.UpdatedAt,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// get file information from the attachment depending on the requested media size
 | 
					 | 
				
			||||||
	switch mediaSize {
 | 
					 | 
				
			||||||
	case media.SizeOriginal:
 | 
					 | 
				
			||||||
		attachmentContent.ContentType = a.File.ContentType
 | 
					 | 
				
			||||||
		attachmentContent.ContentLength = int64(a.File.FileSize)
 | 
					 | 
				
			||||||
		storagePath = a.File.Path
 | 
					 | 
				
			||||||
	case media.SizeSmall:
 | 
					 | 
				
			||||||
		attachmentContent.ContentType = a.Thumbnail.ContentType
 | 
					 | 
				
			||||||
		attachmentContent.ContentLength = int64(a.Thumbnail.FileSize)
 | 
					 | 
				
			||||||
		storagePath = a.Thumbnail.Path
 | 
					 | 
				
			||||||
	default:
 | 
					 | 
				
			||||||
		return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not recognized for attachment", mediaSize))
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// ... so now we can safely return it
 | 
					 | 
				
			||||||
	return p.retrieveFromStorage(ctx, storagePath, attachmentContent)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (p *Processor) getEmojiContent(ctx context.Context, fileName string, owningAccountID string, emojiSize media.Size) (*apimodel.Content, gtserror.WithCode) {
 | 
					 | 
				
			||||||
	emojiContent := &apimodel.Content{}
 | 
					 | 
				
			||||||
	var storagePath string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// reconstruct the static emoji image url -- reason
 | 
					 | 
				
			||||||
	// for using the static URL rather than full size url
 | 
					 | 
				
			||||||
	// is that static emojis are always encoded as png,
 | 
					 | 
				
			||||||
	// so this is more reliable than using full size url
 | 
					 | 
				
			||||||
	imageStaticURL := uris.URIForAttachment(
 | 
					 | 
				
			||||||
		owningAccountID,
 | 
					 | 
				
			||||||
		string(media.TypeEmoji),
 | 
					 | 
				
			||||||
		string(media.SizeStatic),
 | 
					 | 
				
			||||||
		fileName,
 | 
					 | 
				
			||||||
		"png",
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	e, err := p.state.DB.GetEmojiByStaticURL(ctx, imageStaticURL)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s could not be taken from the db: %w", fileName, err))
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if *e.Disabled {
 | 
					 | 
				
			||||||
		return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s has been disabled", fileName))
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if !*e.Cached {
 | 
					 | 
				
			||||||
		// if we don't have it cached, then we can assume two things:
 | 
					 | 
				
			||||||
		// 1. this is remote emoji, since local emoji should never be uncached
 | 
					 | 
				
			||||||
		// 2. we need to fetch it again using a transport and the media manager
 | 
					 | 
				
			||||||
		remoteURL, err := url.Parse(e.ImageRemoteURL)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return nil, gtserror.NewErrorNotFound(fmt.Errorf("error parsing remote emoji iri %s: %w", e.ImageRemoteURL, err))
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		dataFn := func(ctx context.Context) (io.ReadCloser, int64, error) {
 | 
					 | 
				
			||||||
			t, err := p.transportController.NewTransportForUsername(ctx, "")
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return nil, 0, err
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			return t.DereferenceMedia(gtscontext.SetFastFail(ctx), remoteURL)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Start recaching this emoji with the prepared data function.
 | 
					 | 
				
			||||||
		processingEmoji, err := p.mediaManager.PreProcessEmojiRecache(ctx, dataFn, e.ID)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return nil, gtserror.NewErrorNotFound(fmt.Errorf("error recaching emoji: %w", err))
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Load attachment and block until complete
 | 
					 | 
				
			||||||
		e, err = processingEmoji.LoadEmoji(ctx)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return nil, gtserror.NewErrorNotFound(fmt.Errorf("error loading recached emoji: %w", err))
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	switch emojiSize {
 | 
					 | 
				
			||||||
	case media.SizeOriginal:
 | 
					 | 
				
			||||||
		emojiContent.ContentType = e.ImageContentType
 | 
					 | 
				
			||||||
		emojiContent.ContentLength = int64(e.ImageFileSize)
 | 
					 | 
				
			||||||
		storagePath = e.ImagePath
 | 
					 | 
				
			||||||
	case media.SizeStatic:
 | 
					 | 
				
			||||||
		emojiContent.ContentType = e.ImageStaticContentType
 | 
					 | 
				
			||||||
		emojiContent.ContentLength = int64(e.ImageStaticFileSize)
 | 
					 | 
				
			||||||
		storagePath = e.ImageStaticPath
 | 
					 | 
				
			||||||
	default:
 | 
					 | 
				
			||||||
		return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not recognized for emoji", emojiSize))
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return p.retrieveFromStorage(ctx, storagePath, emojiContent)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (p *Processor) retrieveFromStorage(ctx context.Context, storagePath string, content *apimodel.Content) (*apimodel.Content, gtserror.WithCode) {
 | 
					 | 
				
			||||||
	// If running on S3 storage with proxying disabled then
 | 
					 | 
				
			||||||
	// just fetch a pre-signed URL instead of serving the content.
 | 
					 | 
				
			||||||
	if url := p.state.Storage.URL(ctx, storagePath); url != nil {
 | 
					 | 
				
			||||||
		content.URL = url
 | 
					 | 
				
			||||||
		return content, nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	reader, err := p.state.Storage.GetStream(ctx, storagePath)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil, gtserror.NewErrorNotFound(fmt.Errorf("error retrieving from storage: %s", err))
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	content.Content = reader
 | 
					 | 
				
			||||||
	return content, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,24 +18,39 @@
 | 
				
			||||||
package media
 | 
					package media
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"github.com/superseriousbusiness/gotosocial/internal/federation"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/media"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/media"
 | 
				
			||||||
 | 
						"github.com/superseriousbusiness/gotosocial/internal/processing/common"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/state"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/state"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/transport"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/transport"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/typeutils"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/typeutils"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Processor struct {
 | 
					type Processor struct {
 | 
				
			||||||
 | 
						// common processor logic
 | 
				
			||||||
 | 
						c *common.Processor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	state               *state.State
 | 
						state               *state.State
 | 
				
			||||||
	converter           *typeutils.Converter
 | 
						converter           *typeutils.Converter
 | 
				
			||||||
 | 
						federator           *federation.Federator
 | 
				
			||||||
	mediaManager        *media.Manager
 | 
						mediaManager        *media.Manager
 | 
				
			||||||
	transportController transport.Controller
 | 
						transportController transport.Controller
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// New returns a new media processor.
 | 
					// New returns a new media processor.
 | 
				
			||||||
func New(state *state.State, converter *typeutils.Converter, mediaManager *media.Manager, transportController transport.Controller) Processor {
 | 
					func New(
 | 
				
			||||||
 | 
						common *common.Processor,
 | 
				
			||||||
 | 
						state *state.State,
 | 
				
			||||||
 | 
						converter *typeutils.Converter,
 | 
				
			||||||
 | 
						federator *federation.Federator,
 | 
				
			||||||
 | 
						mediaManager *media.Manager,
 | 
				
			||||||
 | 
						transportController transport.Controller,
 | 
				
			||||||
 | 
					) Processor {
 | 
				
			||||||
	return Processor{
 | 
						return Processor{
 | 
				
			||||||
 | 
							c:                   common,
 | 
				
			||||||
		state:               state,
 | 
							state:               state,
 | 
				
			||||||
		converter:           converter,
 | 
							converter:           converter,
 | 
				
			||||||
 | 
							federator:           federator,
 | 
				
			||||||
		mediaManager:        mediaManager,
 | 
							mediaManager:        mediaManager,
 | 
				
			||||||
		transportController: transportController,
 | 
							transportController: transportController,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,8 +20,10 @@ package media_test
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"github.com/stretchr/testify/suite"
 | 
						"github.com/stretchr/testify/suite"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/db"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/db"
 | 
				
			||||||
 | 
						"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/media"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/media"
 | 
				
			||||||
 | 
						"github.com/superseriousbusiness/gotosocial/internal/processing/common"
 | 
				
			||||||
	mediaprocessing "github.com/superseriousbusiness/gotosocial/internal/processing/media"
 | 
						mediaprocessing "github.com/superseriousbusiness/gotosocial/internal/processing/media"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/state"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/state"
 | 
				
			||||||
	"github.com/superseriousbusiness/gotosocial/internal/storage"
 | 
						"github.com/superseriousbusiness/gotosocial/internal/storage"
 | 
				
			||||||
| 
						 | 
					@ -78,7 +80,12 @@ func (suite *MediaStandardTestSuite) SetupTest() {
 | 
				
			||||||
	suite.state.Storage = suite.storage
 | 
						suite.state.Storage = suite.storage
 | 
				
			||||||
	suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
 | 
						suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
 | 
				
			||||||
	suite.transportController = testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../testrig/media"))
 | 
						suite.transportController = testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../testrig/media"))
 | 
				
			||||||
	suite.mediaProcessor = mediaprocessing.New(&suite.state, suite.tc, suite.mediaManager, suite.transportController)
 | 
					
 | 
				
			||||||
 | 
						federator := testrig.NewTestFederator(&suite.state, suite.transportController, suite.mediaManager)
 | 
				
			||||||
 | 
						filter := visibility.NewFilter(&suite.state)
 | 
				
			||||||
 | 
						common := common.New(&suite.state, suite.mediaManager, suite.tc, federator, filter)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						suite.mediaProcessor = mediaprocessing.New(&common, &suite.state, suite.tc, federator, suite.mediaManager, suite.transportController)
 | 
				
			||||||
	testrig.StandardDBSetup(suite.db, nil)
 | 
						testrig.StandardDBSetup(suite.db, nil)
 | 
				
			||||||
	testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
 | 
						testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -57,7 +57,7 @@ func (suite *PollTestSuite) SetupTest() {
 | 
				
			||||||
	mediaMgr := media.NewManager(&suite.state)
 | 
						mediaMgr := media.NewManager(&suite.state)
 | 
				
			||||||
	federator := testrig.NewTestFederator(&suite.state, controller, mediaMgr)
 | 
						federator := testrig.NewTestFederator(&suite.state, controller, mediaMgr)
 | 
				
			||||||
	suite.filter = visibility.NewFilter(&suite.state)
 | 
						suite.filter = visibility.NewFilter(&suite.state)
 | 
				
			||||||
	common := common.New(&suite.state, converter, federator, suite.filter)
 | 
						common := common.New(&suite.state, mediaMgr, converter, federator, suite.filter)
 | 
				
			||||||
	suite.polls = polls.New(&common, &suite.state, converter)
 | 
						suite.polls = polls.New(&common, &suite.state, converter)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -179,15 +179,15 @@ func NewProcessor(
 | 
				
			||||||
	//
 | 
						//
 | 
				
			||||||
	// Start with sub processors that will
 | 
						// Start with sub processors that will
 | 
				
			||||||
	// be required by the workers processor.
 | 
						// be required by the workers processor.
 | 
				
			||||||
	common := common.New(state, converter, federator, filter)
 | 
						common := common.New(state, mediaManager, converter, federator, filter)
 | 
				
			||||||
	processor.account = account.New(&common, state, converter, mediaManager, federator, filter, parseMentionFunc)
 | 
						processor.account = account.New(&common, state, converter, mediaManager, federator, filter, parseMentionFunc)
 | 
				
			||||||
	processor.media = media.New(state, converter, mediaManager, federator.TransportController())
 | 
						processor.media = media.New(&common, state, converter, federator, mediaManager, federator.TransportController())
 | 
				
			||||||
	processor.stream = stream.New(state, oauthServer)
 | 
						processor.stream = stream.New(state, oauthServer)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Instantiate the rest of the sub
 | 
						// Instantiate the rest of the sub
 | 
				
			||||||
	// processors + pin them to this struct.
 | 
						// processors + pin them to this struct.
 | 
				
			||||||
	processor.account = account.New(&common, state, converter, mediaManager, federator, filter, parseMentionFunc)
 | 
						processor.account = account.New(&common, state, converter, mediaManager, federator, filter, parseMentionFunc)
 | 
				
			||||||
	processor.admin = admin.New(state, cleaner, converter, mediaManager, federator.TransportController(), emailSender)
 | 
						processor.admin = admin.New(&common, state, cleaner, federator, converter, mediaManager, federator.TransportController(), emailSender)
 | 
				
			||||||
	processor.fedi = fedi.New(state, &common, converter, federator, filter)
 | 
						processor.fedi = fedi.New(state, &common, converter, federator, filter)
 | 
				
			||||||
	processor.filtersv1 = filtersv1.New(state, converter, &processor.stream)
 | 
						processor.filtersv1 = filtersv1.New(state, converter, &processor.stream)
 | 
				
			||||||
	processor.filtersv2 = filtersv2.New(state, converter, &processor.stream)
 | 
						processor.filtersv2 = filtersv2.New(state, converter, &processor.stream)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -96,7 +96,7 @@ func (suite *StatusStandardTestSuite) SetupTest() {
 | 
				
			||||||
		suite.typeConverter,
 | 
							suite.typeConverter,
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	common := common.New(&suite.state, suite.typeConverter, suite.federator, filter)
 | 
						common := common.New(&suite.state, suite.mediaManager, suite.typeConverter, suite.federator, filter)
 | 
				
			||||||
	polls := polls.New(&common, &suite.state, suite.typeConverter)
 | 
						polls := polls.New(&common, &suite.state, suite.typeConverter)
 | 
				
			||||||
	suite.status = status.New(&suite.state, &common, &polls, suite.federator, suite.typeConverter, filter, processing.GetParseMentionFunc(&suite.state, suite.federator))
 | 
						suite.status = status.New(&suite.state, &common, &polls, suite.federator, suite.typeConverter, filter, processing.GetParseMentionFunc(&suite.state, suite.federator))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -95,7 +95,7 @@ func (d *Driver) PutStream(ctx context.Context, key string, r io.Reader) (int64,
 | 
				
			||||||
	return d.Storage.WriteStream(ctx, key, r)
 | 
						return d.Storage.WriteStream(ctx, key, r)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Remove attempts to remove the supplied key (and corresponding value) from storage.
 | 
					// Delete attempts to remove the supplied key (and corresponding value) from storage.
 | 
				
			||||||
func (d *Driver) Delete(ctx context.Context, key string) error {
 | 
					func (d *Driver) Delete(ctx context.Context, key string) error {
 | 
				
			||||||
	return d.Storage.Remove(ctx, key)
 | 
						return d.Storage.Remove(ctx, key)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1051,7 +1051,7 @@ func (c *Converter) EmojiToAS(ctx context.Context, e *gtsmodel.Emoji) (vocab.Too
 | 
				
			||||||
	emoji.SetActivityStreamsIcon(iconProperty)
 | 
						emoji.SetActivityStreamsIcon(iconProperty)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	updatedProp := streams.NewActivityStreamsUpdatedProperty()
 | 
						updatedProp := streams.NewActivityStreamsUpdatedProperty()
 | 
				
			||||||
	updatedProp.Set(e.ImageUpdatedAt)
 | 
						updatedProp.Set(e.UpdatedAt)
 | 
				
			||||||
	emoji.SetActivityStreamsUpdated(updatedProp)
 | 
						emoji.SetActivityStreamsUpdated(updatedProp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return emoji, nil
 | 
						return emoji, nil
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -49,10 +49,6 @@ type Workers struct {
 | 
				
			||||||
	// for asynchronous dereferencer jobs.
 | 
						// for asynchronous dereferencer jobs.
 | 
				
			||||||
	Dereference FnWorkerPool
 | 
						Dereference FnWorkerPool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Media provides a worker pool for
 | 
					 | 
				
			||||||
	// asynchronous media processing jobs.
 | 
					 | 
				
			||||||
	Media FnWorkerPool
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// prevent pass-by-value.
 | 
						// prevent pass-by-value.
 | 
				
			||||||
	_ nocopy
 | 
						_ nocopy
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -84,10 +80,6 @@ func (w *Workers) Start() {
 | 
				
			||||||
	n = 4 * maxprocs
 | 
						n = 4 * maxprocs
 | 
				
			||||||
	w.Dereference.Start(n)
 | 
						w.Dereference.Start(n)
 | 
				
			||||||
	log.Infof(nil, "started %d dereference workers", n)
 | 
						log.Infof(nil, "started %d dereference workers", n)
 | 
				
			||||||
 | 
					 | 
				
			||||||
	n = 8 * maxprocs
 | 
					 | 
				
			||||||
	w.Media.Start(n)
 | 
					 | 
				
			||||||
	log.Infof(nil, "started %d media workers", n)
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Stop will stop all of the contained worker pools (and global scheduler).
 | 
					// Stop will stop all of the contained worker pools (and global scheduler).
 | 
				
			||||||
| 
						 | 
					@ -105,9 +97,6 @@ func (w *Workers) Stop() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	w.Dereference.Stop()
 | 
						w.Dereference.Stop()
 | 
				
			||||||
	log.Info(nil, "stopped dereference workers")
 | 
						log.Info(nil, "stopped dereference workers")
 | 
				
			||||||
 | 
					 | 
				
			||||||
	w.Media.Stop()
 | 
					 | 
				
			||||||
	log.Info(nil, "stopped media workers")
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// nocopy when embedded will signal linter to
 | 
					// nocopy when embedded will signal linter to
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -739,13 +739,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
 | 
				
			||||||
				Path:        "01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg",
 | 
									Path:        "01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg",
 | 
				
			||||||
				ContentType: "image/jpeg",
 | 
									ContentType: "image/jpeg",
 | 
				
			||||||
				FileSize:    62529,
 | 
									FileSize:    62529,
 | 
				
			||||||
				UpdatedAt:   TimeMustParse("2022-06-04T13:12:00Z"),
 | 
					 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			Thumbnail: gtsmodel.Thumbnail{
 | 
								Thumbnail: gtsmodel.Thumbnail{
 | 
				
			||||||
				Path:        "01F8MH17FWEB39HZJ76B6VXSKF/attachment/small/01F8MH6NEM8D7527KZAECTCR76.jpg",
 | 
									Path:        "01F8MH17FWEB39HZJ76B6VXSKF/attachment/small/01F8MH6NEM8D7527KZAECTCR76.jpg",
 | 
				
			||||||
				ContentType: "image/jpeg",
 | 
									ContentType: "image/jpeg",
 | 
				
			||||||
				FileSize:    6872,
 | 
									FileSize:    6872,
 | 
				
			||||||
				UpdatedAt:   TimeMustParse("2022-06-04T13:12:00Z"),
 | 
					 | 
				
			||||||
				URL:         "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/small/01F8MH6NEM8D7527KZAECTCR76.jpg",
 | 
									URL:         "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/small/01F8MH6NEM8D7527KZAECTCR76.jpg",
 | 
				
			||||||
				RemoteURL:   "",
 | 
									RemoteURL:   "",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
| 
						 | 
					@ -788,13 +786,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
 | 
				
			||||||
				Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01F8MH7TDVANYKWVE8VVKFPJTJ.gif",
 | 
									Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01F8MH7TDVANYKWVE8VVKFPJTJ.gif",
 | 
				
			||||||
				ContentType: "image/gif",
 | 
									ContentType: "image/gif",
 | 
				
			||||||
				FileSize:    1109138,
 | 
									FileSize:    1109138,
 | 
				
			||||||
				UpdatedAt:   TimeMustParse("2022-06-09T13:12:00Z"),
 | 
					 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			Thumbnail: gtsmodel.Thumbnail{
 | 
								Thumbnail: gtsmodel.Thumbnail{
 | 
				
			||||||
				Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01F8MH7TDVANYKWVE8VVKFPJTJ.jpg",
 | 
									Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01F8MH7TDVANYKWVE8VVKFPJTJ.jpg",
 | 
				
			||||||
				ContentType: "image/jpeg",
 | 
									ContentType: "image/jpeg",
 | 
				
			||||||
				FileSize:    8803,
 | 
									FileSize:    8803,
 | 
				
			||||||
				UpdatedAt:   TimeMustParse("2022-06-09T13:12:00Z"),
 | 
					 | 
				
			||||||
				URL:         "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01F8MH7TDVANYKWVE8VVKFPJTJ.jpg",
 | 
									URL:         "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01F8MH7TDVANYKWVE8VVKFPJTJ.jpg",
 | 
				
			||||||
				RemoteURL:   "",
 | 
									RemoteURL:   "",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
| 
						 | 
					@ -840,13 +836,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
 | 
				
			||||||
				Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01CDR64G398ADCHXK08WWTHEZ5.gif",
 | 
									Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01CDR64G398ADCHXK08WWTHEZ5.gif",
 | 
				
			||||||
				ContentType: "video/mp4",
 | 
									ContentType: "video/mp4",
 | 
				
			||||||
				FileSize:    2273532,
 | 
									FileSize:    2273532,
 | 
				
			||||||
				UpdatedAt:   TimeMustParse("2022-06-09T13:12:00Z"),
 | 
					 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			Thumbnail: gtsmodel.Thumbnail{
 | 
								Thumbnail: gtsmodel.Thumbnail{
 | 
				
			||||||
				Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01CDR64G398ADCHXK08WWTHEZ5.jpg",
 | 
									Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01CDR64G398ADCHXK08WWTHEZ5.jpg",
 | 
				
			||||||
				ContentType: "image/jpeg",
 | 
									ContentType: "image/jpeg",
 | 
				
			||||||
				FileSize:    5272,
 | 
									FileSize:    5272,
 | 
				
			||||||
				UpdatedAt:   TimeMustParse("2022-06-09T13:12:00Z"),
 | 
					 | 
				
			||||||
				URL:         "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01CDR64G398ADCHXK08WWTHEZ5.jpg",
 | 
									URL:         "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01CDR64G398ADCHXK08WWTHEZ5.jpg",
 | 
				
			||||||
				RemoteURL:   "",
 | 
									RemoteURL:   "",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
| 
						 | 
					@ -889,13 +883,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
 | 
				
			||||||
				Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01F8MH8RMYQ6MSNY3JM2XT1CQ5.jpg",
 | 
									Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01F8MH8RMYQ6MSNY3JM2XT1CQ5.jpg",
 | 
				
			||||||
				ContentType: "image/jpeg",
 | 
									ContentType: "image/jpeg",
 | 
				
			||||||
				FileSize:    27759,
 | 
									FileSize:    27759,
 | 
				
			||||||
				UpdatedAt:   TimeMustParse("2022-06-09T13:12:00Z"),
 | 
					 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			Thumbnail: gtsmodel.Thumbnail{
 | 
								Thumbnail: gtsmodel.Thumbnail{
 | 
				
			||||||
				Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01F8MH8RMYQ6MSNY3JM2XT1CQ5.jpg",
 | 
									Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01F8MH8RMYQ6MSNY3JM2XT1CQ5.jpg",
 | 
				
			||||||
				ContentType: "image/jpeg",
 | 
									ContentType: "image/jpeg",
 | 
				
			||||||
				FileSize:    6177,
 | 
									FileSize:    6177,
 | 
				
			||||||
				UpdatedAt:   TimeMustParse("2022-06-09T13:12:00Z"),
 | 
					 | 
				
			||||||
				URL:         "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01F8MH8RMYQ6MSNY3JM2XT1CQ5.jpg",
 | 
									URL:         "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01F8MH8RMYQ6MSNY3JM2XT1CQ5.jpg",
 | 
				
			||||||
				RemoteURL:   "",
 | 
									RemoteURL:   "",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
| 
						 | 
					@ -938,13 +930,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
 | 
				
			||||||
				Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
 | 
									Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
 | 
				
			||||||
				ContentType: "image/jpeg",
 | 
									ContentType: "image/jpeg",
 | 
				
			||||||
				FileSize:    457680,
 | 
									FileSize:    457680,
 | 
				
			||||||
				UpdatedAt:   TimeMustParse("2022-06-09T13:12:00Z"),
 | 
					 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			Thumbnail: gtsmodel.Thumbnail{
 | 
								Thumbnail: gtsmodel.Thumbnail{
 | 
				
			||||||
				Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
 | 
									Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
 | 
				
			||||||
				ContentType: "image/jpeg",
 | 
									ContentType: "image/jpeg",
 | 
				
			||||||
				FileSize:    15374,
 | 
									FileSize:    15374,
 | 
				
			||||||
				UpdatedAt:   TimeMustParse("2022-06-09T13:12:00Z"),
 | 
					 | 
				
			||||||
				URL:         "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
 | 
									URL:         "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
 | 
				
			||||||
				RemoteURL:   "",
 | 
									RemoteURL:   "",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
| 
						 | 
					@ -987,13 +977,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
 | 
				
			||||||
				Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
 | 
									Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
 | 
				
			||||||
				ContentType: "image/jpeg",
 | 
									ContentType: "image/jpeg",
 | 
				
			||||||
				FileSize:    517226,
 | 
									FileSize:    517226,
 | 
				
			||||||
				UpdatedAt:   TimeMustParse("2022-06-09T13:12:00Z"),
 | 
					 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			Thumbnail: gtsmodel.Thumbnail{
 | 
								Thumbnail: gtsmodel.Thumbnail{
 | 
				
			||||||
				Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
 | 
									Path:        "01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
 | 
				
			||||||
				ContentType: "image/jpeg",
 | 
									ContentType: "image/jpeg",
 | 
				
			||||||
				FileSize:    42308,
 | 
									FileSize:    42308,
 | 
				
			||||||
				UpdatedAt:   TimeMustParse("2022-06-09T13:12:00Z"),
 | 
					 | 
				
			||||||
				URL:         "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
 | 
									URL:         "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
 | 
				
			||||||
				RemoteURL:   "",
 | 
									RemoteURL:   "",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
| 
						 | 
					@ -1036,13 +1024,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
 | 
				
			||||||
				Path:        "01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
 | 
									Path:        "01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
 | 
				
			||||||
				ContentType: "image/jpeg",
 | 
									ContentType: "image/jpeg",
 | 
				
			||||||
				FileSize:    19310,
 | 
									FileSize:    19310,
 | 
				
			||||||
				UpdatedAt:   TimeMustParse("2021-09-20T12:40:37+02:00"),
 | 
					 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			Thumbnail: gtsmodel.Thumbnail{
 | 
								Thumbnail: gtsmodel.Thumbnail{
 | 
				
			||||||
				Path:        "01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
 | 
									Path:        "01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
 | 
				
			||||||
				ContentType: "image/jpeg",
 | 
									ContentType: "image/jpeg",
 | 
				
			||||||
				FileSize:    19312,
 | 
									FileSize:    19312,
 | 
				
			||||||
				UpdatedAt:   TimeMustParse("2021-09-20T12:40:37+02:00"),
 | 
					 | 
				
			||||||
				URL:         "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
 | 
									URL:         "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
 | 
				
			||||||
				RemoteURL:   "http://fossbros-anonymous.io/attachments/small/a499f55b-2d1e-4acd-98d2-1ac2ba6d79b9.jpg",
 | 
									RemoteURL:   "http://fossbros-anonymous.io/attachments/small/a499f55b-2d1e-4acd-98d2-1ac2ba6d79b9.jpg",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
| 
						 | 
					@ -1085,13 +1071,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
 | 
				
			||||||
				Path:        "062G5WYKY35KKD12EMSM3F8PJ8/attachment/original/01PFPMWK2FF0D9WMHEJHR07C3R.jpg",
 | 
									Path:        "062G5WYKY35KKD12EMSM3F8PJ8/attachment/original/01PFPMWK2FF0D9WMHEJHR07C3R.jpg",
 | 
				
			||||||
				ContentType: "image/jpeg",
 | 
									ContentType: "image/jpeg",
 | 
				
			||||||
				FileSize:    19310,
 | 
									FileSize:    19310,
 | 
				
			||||||
				UpdatedAt:   TimeMustParse("2022-06-09T13:12:00Z"),
 | 
					 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			Thumbnail: gtsmodel.Thumbnail{
 | 
								Thumbnail: gtsmodel.Thumbnail{
 | 
				
			||||||
				Path:        "062G5WYKY35KKD12EMSM3F8PJ8/attachment/small/01PFPMWK2FF0D9WMHEJHR07C3R.jpg",
 | 
									Path:        "062G5WYKY35KKD12EMSM3F8PJ8/attachment/small/01PFPMWK2FF0D9WMHEJHR07C3R.jpg",
 | 
				
			||||||
				ContentType: "image/jpeg",
 | 
									ContentType: "image/jpeg",
 | 
				
			||||||
				FileSize:    20395,
 | 
									FileSize:    20395,
 | 
				
			||||||
				UpdatedAt:   TimeMustParse("2022-06-09T13:12:00Z"),
 | 
					 | 
				
			||||||
				URL:         "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/small/01PFPMWK2FF0D9WMHEJHR07C3R.jpg",
 | 
									URL:         "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/small/01PFPMWK2FF0D9WMHEJHR07C3R.jpg",
 | 
				
			||||||
				RemoteURL:   "http://fossbros-anonymous.io/attachments/small/a499f55b-2d1e-4acd-98d2-1ac2ba6d79b9.jpg",
 | 
									RemoteURL:   "http://fossbros-anonymous.io/attachments/small/a499f55b-2d1e-4acd-98d2-1ac2ba6d79b9.jpg",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
| 
						 | 
					@ -1133,13 +1117,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
 | 
				
			||||||
				Path:        "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE7Y3C432WRSNS10EZM86SA5.jpg",
 | 
									Path:        "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE7Y3C432WRSNS10EZM86SA5.jpg",
 | 
				
			||||||
				ContentType: "image/jpg",
 | 
									ContentType: "image/jpg",
 | 
				
			||||||
				FileSize:    5450054,
 | 
									FileSize:    5450054,
 | 
				
			||||||
				UpdatedAt:   TimeMustParse("2023-11-02T12:44:25+02:00"),
 | 
					 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			Thumbnail: gtsmodel.Thumbnail{
 | 
								Thumbnail: gtsmodel.Thumbnail{
 | 
				
			||||||
				Path:        "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE7Y3C432WRSNS10EZM86SA5.jpg",
 | 
									Path:        "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE7Y3C432WRSNS10EZM86SA5.jpg",
 | 
				
			||||||
				ContentType: "image/jpeg",
 | 
									ContentType: "image/jpeg",
 | 
				
			||||||
				FileSize:    50820,
 | 
									FileSize:    50820,
 | 
				
			||||||
				UpdatedAt:   TimeMustParse("2023-11-02T12:44:25+02:00"),
 | 
					 | 
				
			||||||
				URL:         "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE7Y3C432WRSNS10EZM86SA5.jpg",
 | 
									URL:         "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE7Y3C432WRSNS10EZM86SA5.jpg",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			Avatar: util.Ptr(false),
 | 
								Avatar: util.Ptr(false),
 | 
				
			||||||
| 
						 | 
					@ -1163,13 +1145,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
 | 
				
			||||||
				Path:        "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE7ZFX9GKA5ZZVD4FACABSS9.svg",
 | 
									Path:        "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE7ZFX9GKA5ZZVD4FACABSS9.svg",
 | 
				
			||||||
				ContentType: "image/svg",
 | 
									ContentType: "image/svg",
 | 
				
			||||||
				FileSize:    147819,
 | 
									FileSize:    147819,
 | 
				
			||||||
				UpdatedAt:   TimeMustParse("2023-11-02T12:44:25+02:00"),
 | 
					 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			Thumbnail: gtsmodel.Thumbnail{
 | 
								Thumbnail: gtsmodel.Thumbnail{
 | 
				
			||||||
				Path:        "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE7ZFX9GKA5ZZVD4FACABSS9.jpg",
 | 
									Path:        "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE7ZFX9GKA5ZZVD4FACABSS9.jpg",
 | 
				
			||||||
				ContentType: "image/jpeg",
 | 
									ContentType: "image/jpeg",
 | 
				
			||||||
				FileSize:    0,
 | 
									FileSize:    0,
 | 
				
			||||||
				UpdatedAt:   TimeMustParse("2023-11-02T12:44:25+02:00"),
 | 
					 | 
				
			||||||
				URL:         "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE7ZFX9GKA5ZZVD4FACABSS9.jpg",
 | 
									URL:         "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE7ZFX9GKA5ZZVD4FACABSS9.jpg",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			Avatar: util.Ptr(false),
 | 
								Avatar: util.Ptr(false),
 | 
				
			||||||
| 
						 | 
					@ -1193,13 +1173,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
 | 
				
			||||||
				Path:        "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE88YG74PVAB81PX2XA9F3FG.mp3",
 | 
									Path:        "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE88YG74PVAB81PX2XA9F3FG.mp3",
 | 
				
			||||||
				ContentType: "audio/mpeg",
 | 
									ContentType: "audio/mpeg",
 | 
				
			||||||
				FileSize:    147819,
 | 
									FileSize:    147819,
 | 
				
			||||||
				UpdatedAt:   TimeMustParse("2023-11-02T12:44:25+02:00"),
 | 
					 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			Thumbnail: gtsmodel.Thumbnail{
 | 
								Thumbnail: gtsmodel.Thumbnail{
 | 
				
			||||||
				Path:        "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE88YG74PVAB81PX2XA9F3FG.jpg",
 | 
									Path:        "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE88YG74PVAB81PX2XA9F3FG.jpg",
 | 
				
			||||||
				ContentType: "image/jpeg",
 | 
									ContentType: "image/jpeg",
 | 
				
			||||||
				FileSize:    0,
 | 
									FileSize:    0,
 | 
				
			||||||
				UpdatedAt:   TimeMustParse("2023-11-02T12:44:25+02:00"),
 | 
					 | 
				
			||||||
				URL:         "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE88YG74PVAB81PX2XA9F3FG.jpg",
 | 
									URL:         "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE88YG74PVAB81PX2XA9F3FG.jpg",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			Avatar: util.Ptr(false),
 | 
								Avatar: util.Ptr(false),
 | 
				
			||||||
| 
						 | 
					@ -1228,7 +1206,6 @@ func NewTestEmojis() map[string]*gtsmodel.Emoji {
 | 
				
			||||||
			ImageStaticContentType: "image/png",
 | 
								ImageStaticContentType: "image/png",
 | 
				
			||||||
			ImageFileSize:          36702,
 | 
								ImageFileSize:          36702,
 | 
				
			||||||
			ImageStaticFileSize:    10413,
 | 
								ImageStaticFileSize:    10413,
 | 
				
			||||||
			ImageUpdatedAt:         TimeMustParse("2021-09-20T12:40:37+02:00"),
 | 
					 | 
				
			||||||
			Disabled:               util.Ptr(false),
 | 
								Disabled:               util.Ptr(false),
 | 
				
			||||||
			URI:                    "http://localhost:8080/emoji/01F8MH9H8E4VG3KDYJR9EGPXCQ",
 | 
								URI:                    "http://localhost:8080/emoji/01F8MH9H8E4VG3KDYJR9EGPXCQ",
 | 
				
			||||||
			VisibleInPicker:        util.Ptr(true),
 | 
								VisibleInPicker:        util.Ptr(true),
 | 
				
			||||||
| 
						 | 
					@ -1251,7 +1228,6 @@ func NewTestEmojis() map[string]*gtsmodel.Emoji {
 | 
				
			||||||
			ImageStaticContentType: "image/png",
 | 
								ImageStaticContentType: "image/png",
 | 
				
			||||||
			ImageFileSize:          10889,
 | 
								ImageFileSize:          10889,
 | 
				
			||||||
			ImageStaticFileSize:    10808,
 | 
								ImageStaticFileSize:    10808,
 | 
				
			||||||
			ImageUpdatedAt:         TimeMustParse("2020-03-18T13:12:00+01:00"),
 | 
					 | 
				
			||||||
			Disabled:               util.Ptr(false),
 | 
								Disabled:               util.Ptr(false),
 | 
				
			||||||
			URI:                    "http://fossbros-anonymous.io/emoji/01GD5KP5CQEE1R3X43Y1EHS2CW",
 | 
								URI:                    "http://fossbros-anonymous.io/emoji/01GD5KP5CQEE1R3X43Y1EHS2CW",
 | 
				
			||||||
			VisibleInPicker:        util.Ptr(false),
 | 
								VisibleInPicker:        util.Ptr(false),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -82,7 +82,6 @@ func StartWorkers(state *state.State, processor *workers.Processor) {
 | 
				
			||||||
	state.Workers.Client.Start(1)
 | 
						state.Workers.Client.Start(1)
 | 
				
			||||||
	state.Workers.Federator.Start(1)
 | 
						state.Workers.Federator.Start(1)
 | 
				
			||||||
	state.Workers.Dereference.Start(1)
 | 
						state.Workers.Dereference.Start(1)
 | 
				
			||||||
	state.Workers.Media.Start(1)
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func StopWorkers(state *state.State) {
 | 
					func StopWorkers(state *state.State) {
 | 
				
			||||||
| 
						 | 
					@ -90,7 +89,6 @@ func StopWorkers(state *state.State) {
 | 
				
			||||||
	state.Workers.Client.Stop()
 | 
						state.Workers.Client.Stop()
 | 
				
			||||||
	state.Workers.Federator.Stop()
 | 
						state.Workers.Federator.Stop()
 | 
				
			||||||
	state.Workers.Dereference.Stop()
 | 
						state.Workers.Dereference.Stop()
 | 
				
			||||||
	state.Workers.Media.Stop()
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func StartTimelines(state *state.State, filter *visibility.Filter, converter *typeutils.Converter) {
 | 
					func StartTimelines(state *state.State, filter *visibility.Filter, converter *typeutils.Converter) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue