[chore] store basic error details when failing to process remote media (#4625)
- drops `cached` columns from both media and emoji tables, instead relying on empty path - drops `processed` column from media table, as it was totally unused - adds `error` uint32 column to both media and emoji tables, and tracks basic error details encountered when trying to process remote media / emoji - updates our media / emoji processor getter functions to attempt retries only if error situation is expected to be non-permanent - adds a new `error` field to api attachment models to include error details string if failed to download - updates media placeholder text generation to include error details string Todos: - ~~update cleaner to handle the new manner of `cached` flag (i.e. paths being set)~~ - ~~update tests~~ Nice-to-haves in later PRs: - add 'force' flag for admins on media / emoji getter endpoints to allow forcing retry regardless of error type Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4625 Co-authored-by: kim <grufwub@gmail.com> Co-committed-by: kim <grufwub@gmail.com>
This commit is contained in:
@@ -93,7 +93,7 @@ func (l *list) ListAttachmentPaths(ctx context.Context) error {
|
||||
|
||||
for {
|
||||
// Get next page of media attachments up to max ID.
|
||||
medias, err := l.state.DB.GetAttachments(ctx, "", &page)
|
||||
medias, err := l.state.DB.GetAttachments(ctx, &page)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
return fmt.Errorf("failed to fetch media from database: %w", err)
|
||||
}
|
||||
|
||||
@@ -172,8 +172,9 @@ func Start(ctx context.Context) error {
|
||||
log.Info(ctx, "done! exiting...")
|
||||
}()
|
||||
|
||||
// Create maintenance router.
|
||||
var err error
|
||||
|
||||
// Create maintenance router.
|
||||
route, err = router.New(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating maintenance router: %w", err)
|
||||
|
||||
@@ -992,6 +992,11 @@ definitions:
|
||||
example: This is a picture of a kitten.
|
||||
type: string
|
||||
x-go-name: Description
|
||||
error:
|
||||
description: Error encountered while fetching remote media, if any.
|
||||
example: network timeout
|
||||
type: string
|
||||
x-go-name: Error
|
||||
id:
|
||||
description: The ID of the attachment.
|
||||
example: 01FC31DZT1AYWDZ8XTCRWRBYRK
|
||||
|
||||
@@ -38,6 +38,7 @@ import (
|
||||
|
||||
// ExtractObjects will extract object TypeOrIRIs from given implementing interface.
|
||||
func ExtractObjects(with WithObject) []TypeOrIRI {
|
||||
|
||||
// Extract the attached object (if any).
|
||||
objProp := with.GetActivityStreamsObject()
|
||||
if objProp == nil {
|
||||
@@ -60,6 +61,7 @@ func ExtractObjects(with WithObject) []TypeOrIRI {
|
||||
|
||||
// ExtractInstrument will extract instrument TypeOrIRIs from given implementing interface.
|
||||
func ExtractInstruments(with WithInstrument) []TypeOrIRI {
|
||||
|
||||
// Extract the attached instrument (if any).
|
||||
instrProp := with.GetActivityStreamsInstrument()
|
||||
if instrProp == nil {
|
||||
@@ -800,7 +802,6 @@ func ExtractAttachment(i Attachmentable) (*gtsmodel.MediaAttachment, error) {
|
||||
FileMeta: gtsmodel.FileMeta{
|
||||
Focus: ExtractFocus(i),
|
||||
},
|
||||
Processing: gtsmodel.ProcessingStatusReceived,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -532,10 +532,10 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() {
|
||||
"url": "http://thequeenisstillalive.technology/@her_fuckin_maj",
|
||||
"avatar": "",
|
||||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/original/01PFPMWK2FF0D9WMHEJHR07C3R.jpg",
|
||||
"header_static": "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/small/01PFPMWK2FF0D9WMHEJHR07C3R.webp",
|
||||
"header": "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/original/01G549FP8065NKWBPTWHP6Y3PD.jpg",
|
||||
"header_static": "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/small/01G549FP8065NKWBPTWHP6Y3PD.webp",
|
||||
"header_description": "tweet from thoughts of dog: i drank. all the water. in my bowl. earlier. but just now. i returned. to the same bowl. and it was. full again.. the bowl. is haunted",
|
||||
"header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3R",
|
||||
"header_media_id": "01G549FP8065NKWBPTWHP6Y3PD",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 0,
|
||||
|
||||
@@ -35,7 +35,7 @@ type MediaCleanupTestSuite struct {
|
||||
|
||||
func (suite *MediaCleanupTestSuite) TestMediaCleanup() {
|
||||
testAttachment := suite.testAttachments["remote_account_1_status_1_attachment_1"]
|
||||
suite.True(*testAttachment.Cached)
|
||||
suite.True(testAttachment.Cached())
|
||||
|
||||
// set up the request
|
||||
recorder := httptest.NewRecorder()
|
||||
@@ -50,7 +50,7 @@ func (suite *MediaCleanupTestSuite) TestMediaCleanup() {
|
||||
// the attachment should be updated in the database
|
||||
if !testrig.WaitFor(func() bool {
|
||||
if prunedAttachment, _ := suite.db.GetAttachmentByID(suite.T().Context(), testAttachment.ID); prunedAttachment != nil {
|
||||
return !*prunedAttachment.Cached
|
||||
return !prunedAttachment.Cached()
|
||||
}
|
||||
return false
|
||||
}) {
|
||||
@@ -60,7 +60,7 @@ func (suite *MediaCleanupTestSuite) TestMediaCleanup() {
|
||||
|
||||
func (suite *MediaCleanupTestSuite) TestMediaCleanupNoArg() {
|
||||
testAttachment := suite.testAttachments["remote_account_1_status_1_attachment_1"]
|
||||
suite.True(*testAttachment.Cached)
|
||||
suite.True(testAttachment.Cached())
|
||||
println("TIME: ", testAttachment.CreatedAt.String())
|
||||
|
||||
// set up the request
|
||||
@@ -75,7 +75,7 @@ func (suite *MediaCleanupTestSuite) TestMediaCleanupNoArg() {
|
||||
|
||||
if !testrig.WaitFor(func() bool {
|
||||
if prunedAttachment, _ := suite.db.GetAttachmentByID(suite.T().Context(), testAttachment.ID); prunedAttachment != nil {
|
||||
return !*prunedAttachment.Cached
|
||||
return !prunedAttachment.Cached()
|
||||
}
|
||||
return false
|
||||
}) {
|
||||
@@ -85,7 +85,7 @@ func (suite *MediaCleanupTestSuite) TestMediaCleanupNoArg() {
|
||||
|
||||
func (suite *MediaCleanupTestSuite) TestMediaCleanupNotOldEnough() {
|
||||
testAttachment := suite.testAttachments["remote_account_1_status_1_attachment_1"]
|
||||
suite.True(*testAttachment.Cached)
|
||||
suite.True(testAttachment.Cached())
|
||||
|
||||
// set up the request
|
||||
recorder := httptest.NewRecorder()
|
||||
@@ -105,12 +105,12 @@ func (suite *MediaCleanupTestSuite) TestMediaCleanupNotOldEnough() {
|
||||
suite.NoError(err)
|
||||
|
||||
// the media should still be cached
|
||||
suite.True(*prunedAttachment.Cached)
|
||||
suite.True(prunedAttachment.Cached())
|
||||
}
|
||||
|
||||
func (suite *MediaCleanupTestSuite) TestMediaCleanupNegative() {
|
||||
testAttachment := suite.testAttachments["remote_account_1_status_1_attachment_1"]
|
||||
suite.True(*testAttachment.Cached)
|
||||
suite.True(testAttachment.Cached())
|
||||
|
||||
// Set up the request
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
@@ -68,10 +68,22 @@ func (suite *FileserverTestSuite) SetupSuite() {
|
||||
|
||||
testrig.InitTestConfig()
|
||||
testrig.InitTestLog()
|
||||
}
|
||||
|
||||
func (suite *FileserverTestSuite) SetupTest() {
|
||||
suite.state.Caches.Init()
|
||||
testrig.StartNoopWorkers(&suite.state)
|
||||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
|
||||
|
||||
suite.storage = testrig.NewInMemoryStorage()
|
||||
suite.state.Storage = suite.storage
|
||||
|
||||
testrig.StandardDBSetup(suite.db, nil)
|
||||
testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
|
||||
|
||||
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
|
||||
suite.federator = testrig.NewTestFederator(
|
||||
&suite.state,
|
||||
@@ -96,18 +108,6 @@ func (suite *FileserverTestSuite) SetupSuite() {
|
||||
suite.emailSender = testrig.NewEmailSender("../../../web/template/", nil)
|
||||
|
||||
suite.fileServer = fileserver.New(suite.processor)
|
||||
}
|
||||
|
||||
func (suite *FileserverTestSuite) SetupTest() {
|
||||
suite.state.Caches.Init()
|
||||
testrig.StartNoopWorkers(&suite.state)
|
||||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
|
||||
|
||||
testrig.StandardDBSetup(suite.db, nil)
|
||||
testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
|
||||
|
||||
suite.testTokens = testrig.NewTestTokens()
|
||||
suite.testApplications = testrig.NewTestApplications()
|
||||
|
||||
@@ -75,18 +75,22 @@ func (suite *ServeFileTestSuite) GetFile(
|
||||
func (suite *ServeFileTestSuite) UncacheAttachment(targetAttachment *gtsmodel.MediaAttachment) {
|
||||
ctx := suite.T().Context()
|
||||
|
||||
cached := false
|
||||
targetAttachment.Cached = &cached
|
||||
|
||||
if err := suite.db.UpdateByID(ctx, targetAttachment, targetAttachment.ID, "cached"); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
if err := suite.storage.Delete(ctx, targetAttachment.File.Path); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
if err := suite.storage.Delete(ctx, targetAttachment.Thumbnail.Path); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
targetAttachment.File.Path = ""
|
||||
targetAttachment.Thumbnail.Path = ""
|
||||
|
||||
if err := suite.db.UpdateAttachment(ctx, targetAttachment,
|
||||
"thumbnail_path",
|
||||
"file_path",
|
||||
); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ServeFileTestSuite) TestServeOriginalLocalFileOK() {
|
||||
@@ -177,7 +181,8 @@ func (suite *ServeFileTestSuite) TestServeOriginalRemoteFileRecache() {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// uncache the attachment so we'll have to refetch it from the 'remote' instance
|
||||
// uncache the attachment so we'll have to
|
||||
// refetch it from the 'remote' instance
|
||||
suite.UncacheAttachment(targetAttachment)
|
||||
|
||||
code, headers, body := suite.GetFile(
|
||||
@@ -200,7 +205,8 @@ func (suite *ServeFileTestSuite) TestServeSmallRemoteFileRecache() {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// uncache the attachment so we'll have to refetch it from the 'remote' instance
|
||||
// uncache the attachment so we'll have to
|
||||
// refetch it from the 'remote' instance
|
||||
suite.UncacheAttachment(targetAttachment)
|
||||
|
||||
code, headers, body := suite.GetFile(
|
||||
@@ -219,10 +225,11 @@ func (suite *ServeFileTestSuite) TestServeOriginalRemoteFileRecacheNotFound() {
|
||||
targetAttachment := >smodel.MediaAttachment{}
|
||||
*targetAttachment = *suite.testAttachments["remote_account_1_status_1_attachment_1"]
|
||||
|
||||
// uncache the attachment *and* set the remote URL to something that will return a 404
|
||||
// uncache the attachment *and* set the remote
|
||||
// URL to something that will return a 404
|
||||
suite.UncacheAttachment(targetAttachment)
|
||||
targetAttachment.RemoteURL = "http://nothing.at.this.url/weeeeeeeee"
|
||||
if err := suite.db.UpdateByID(suite.T().Context(), targetAttachment, targetAttachment.ID, "remote_url"); err != nil {
|
||||
if err := suite.db.UpdateAttachment(suite.T().Context(), targetAttachment); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
@@ -240,10 +247,11 @@ func (suite *ServeFileTestSuite) TestServeSmallRemoteFileRecacheNotFound() {
|
||||
targetAttachment := >smodel.MediaAttachment{}
|
||||
*targetAttachment = *suite.testAttachments["remote_account_1_status_1_attachment_1"]
|
||||
|
||||
// uncache the attachment *and* set the remote URL to something that will return a 404
|
||||
// uncache the attachment *and* set the remote
|
||||
// URL to something that will return a 404
|
||||
suite.UncacheAttachment(targetAttachment)
|
||||
targetAttachment.RemoteURL = "http://nothing.at.this.url/weeeeeeeee"
|
||||
if err := suite.db.UpdateByID(suite.T().Context(), targetAttachment, targetAttachment.ID, "remote_url"); err != nil {
|
||||
if err := suite.db.UpdateAttachment(suite.T().Context(), targetAttachment); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
|
||||
@@ -78,9 +78,11 @@ type AttachmentAttributesRequest struct {
|
||||
//
|
||||
// swagger:model attachment
|
||||
type Attachment struct {
|
||||
|
||||
// The ID of the attachment.
|
||||
// example: 01FC31DZT1AYWDZ8XTCRWRBYRK
|
||||
ID string `json:"id"`
|
||||
|
||||
// The type of the attachment.
|
||||
// enum:
|
||||
// - unknown
|
||||
@@ -90,31 +92,43 @@ type Attachment struct {
|
||||
// - audio
|
||||
// example: image
|
||||
Type string `json:"type"`
|
||||
|
||||
// The location of the original full-size attachment.
|
||||
// example: https://example.org/fileserver/some_id/attachments/some_id/original/attachment.jpeg
|
||||
URL *string `json:"url"`
|
||||
|
||||
// A shorter URL for the attachment.
|
||||
// In our case, we just give the URL again since we don't create smaller URLs.
|
||||
TextURL *string `json:"text_url"`
|
||||
|
||||
// The location of a scaled-down preview of the attachment.
|
||||
// example: https://example.org/fileserver/some_id/attachments/some_id/small/attachment.jpeg
|
||||
PreviewURL *string `json:"preview_url"`
|
||||
|
||||
// The location of the full-size original attachment on the remote server.
|
||||
// Only defined for instances other than our own.
|
||||
// example: https://some-other-server.org/attachments/original/ahhhhh.jpeg
|
||||
RemoteURL *string `json:"remote_url"`
|
||||
|
||||
// The location of a scaled-down preview of the attachment on the remote server.
|
||||
// Only defined for instances other than our own.
|
||||
// example: https://some-other-server.org/attachments/small/ahhhhh.jpeg
|
||||
PreviewRemoteURL *string `json:"preview_remote_url"`
|
||||
|
||||
// Metadata for this attachment.
|
||||
Meta *MediaMeta `json:"meta"`
|
||||
|
||||
// Alt text that describes what is in the media attachment.
|
||||
// example: This is a picture of a kitten.
|
||||
Description *string `json:"description"`
|
||||
|
||||
// A hash computed by the BlurHash algorithm, for generating colorful preview thumbnails when media has not been downloaded yet.
|
||||
// See https://github.com/woltapp/blurhash
|
||||
Blurhash *string `json:"blurhash"`
|
||||
|
||||
// Error encountered while fetching remote media, if any.
|
||||
// example: network timeout
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// WebAttachment is like Attachment, but with
|
||||
|
||||
Vendored
-2
@@ -348,7 +348,6 @@ func sizeofEmoji() uintptr {
|
||||
URI: "http://localhost:8080/emoji/01F8MH9H8E4VG3KDYJR9EGPXCQ",
|
||||
VisibleInPicker: func() *bool { ok := true; return &ok }(),
|
||||
CategoryID: "01GGQ8V4993XK67B2JB396YFB7",
|
||||
Cached: func() *bool { ok := true; return &ok }(),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -490,7 +489,6 @@ func sizeofMedia() uintptr {
|
||||
},
|
||||
Avatar: func() *bool { ok := false; return &ok }(),
|
||||
Header: func() *bool { ok := false; return &ok }(),
|
||||
Cached: func() *bool { ok := true; return &ok }(),
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -28,7 +28,7 @@ import (
|
||||
// plus1hULID returns a ULID for now+1h.
|
||||
func plus1hULID() string {
|
||||
t := time.Now().Add(time.Hour)
|
||||
return id.NewULIDFromTime(t)
|
||||
return id.ZeroULIDForTime(t)
|
||||
}
|
||||
|
||||
// nextPageParams gets the next set of paging
|
||||
|
||||
+38
-48
@@ -27,9 +27,8 @@ import (
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtscontext"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/id"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/paging"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/storage"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// Emoji encompasses a set of
|
||||
@@ -107,37 +106,42 @@ func (e *Emoji) LogFixCacheStates(ctx context.Context) {
|
||||
// will be checked for `gtscontext.DryRun()` in order to actually perform the action.
|
||||
func (e *Emoji) UncacheRemote(ctx context.Context, olderThan time.Time) (int, error) {
|
||||
var total int
|
||||
var page paging.Page
|
||||
|
||||
// Setup page w/ select limit.
|
||||
page.Max = paging.MaxID("")
|
||||
page.Limit = selectLimit
|
||||
|
||||
// Drop time by a minute to improve search,
|
||||
// (i.e. make it olderThan inclusive search).
|
||||
olderThan = olderThan.Add(-time.Minute)
|
||||
|
||||
// Store recent time.
|
||||
mostRecent := olderThan
|
||||
// Get ULID for 'olderThan' to use as maxID.
|
||||
olderThanID := id.ZeroULIDForTime(olderThan)
|
||||
page.Max.Value = olderThanID
|
||||
|
||||
for {
|
||||
// Fetch the next batch of cached emojis older than last-set time.
|
||||
emojis, err := e.state.DB.GetCachedEmojisOlderThan(ctx, olderThan, selectLimit)
|
||||
// Fetch next batch of cached emojis older than maxID.
|
||||
emojis, err := e.state.DB.GetCachedEmojis(ctx, &page)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
return total, gtserror.Newf("error getting remote emoji: %w", err)
|
||||
}
|
||||
|
||||
// If no emojis / same group is
|
||||
// returned, we reached the end.
|
||||
if len(emojis) == 0 ||
|
||||
olderThan.Equal(emojis[len(emojis)-1].CreatedAt) {
|
||||
// Get current max ID.
|
||||
maxID := page.Max.Value
|
||||
|
||||
// If none or the same group is returned, we reached the end.
|
||||
if len(emojis) == 0 || maxID == emojis[len(emojis)-1].ID {
|
||||
break
|
||||
}
|
||||
|
||||
// Use last createdAt as next 'olderThan' value.
|
||||
olderThan = emojis[len(emojis)-1].CreatedAt
|
||||
// Use last ID as next 'maxID' value.
|
||||
maxID = emojis[len(emojis)-1].ID
|
||||
page.Max.Value = maxID
|
||||
|
||||
for _, emoji := range emojis {
|
||||
// Check / uncache each remote emoji.
|
||||
uncached, err := e.uncacheRemote(ctx,
|
||||
mostRecent,
|
||||
emoji,
|
||||
)
|
||||
// Check and try uncache each remote emoji media model.
|
||||
uncached, err := e.uncacheRemote(ctx, olderThan, emoji)
|
||||
if err != nil {
|
||||
return total, err
|
||||
}
|
||||
@@ -199,34 +203,17 @@ func (e *Emoji) PurgeRemote(ctx context.Context, domain string) (int, error) {
|
||||
}
|
||||
|
||||
for _, emoji := range emojis {
|
||||
if emoji.ImagePath != "" {
|
||||
// Ensure emoji file at path is deleted from storage.
|
||||
err := e.state.Storage.Delete(ctx, emoji.ImagePath)
|
||||
if err != nil && !storage.IsNotFound(err) {
|
||||
log.Errorf(ctx, "error deleting %s: %v", emoji.ImagePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
if emoji.ImageStaticPath != "" {
|
||||
// Ensure emoji static file at path is deleted from storage.
|
||||
err := e.state.Storage.Delete(ctx, emoji.ImageStaticPath)
|
||||
if err != nil && !storage.IsNotFound(err) {
|
||||
log.Errorf(ctx, "error deleting %s: %v", emoji.ImageStaticPath, err)
|
||||
}
|
||||
// Remove emoji and static files.
|
||||
_, err := e.removeFiles(ctx,
|
||||
emoji.ImageStaticPath,
|
||||
emoji.ImagePath,
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "error removing emoji files: %v", err)
|
||||
}
|
||||
|
||||
// Unset fields.
|
||||
emoji.ImageStaticContentType = ""
|
||||
emoji.ImageStaticFileSize = 0
|
||||
emoji.ImageStaticPath = ""
|
||||
emoji.ImageStaticURL = ""
|
||||
emoji.ImageContentType = ""
|
||||
emoji.ImageFileSize = 0
|
||||
emoji.ImagePath = ""
|
||||
emoji.ImageURL = ""
|
||||
|
||||
// Ensure marked as not cached.
|
||||
emoji.Cached = util.Ptr(false)
|
||||
emoji.Stub()
|
||||
|
||||
// Update.
|
||||
if err := e.state.DB.UpdateEmoji(ctx, emoji); err != nil {
|
||||
@@ -433,13 +420,13 @@ func (e *Emoji) fixCacheState(ctx context.Context, emoji *gtsmodel.Emoji) (bool,
|
||||
return false, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case *emoji.Cached && !exist:
|
||||
switch cached := emoji.Cached(); {
|
||||
case cached && !exist:
|
||||
// Mark as uncached if expected files don't exist.
|
||||
l.Debug("cached=true exists=false => marking uncached")
|
||||
return true, e.uncache(ctx, emoji)
|
||||
|
||||
case !*emoji.Cached && exist:
|
||||
case !cached && exist:
|
||||
// Remove files if we don't expect them to exist.
|
||||
l.Debug("cached=false exists=true => removing files")
|
||||
_, err := e.removeFiles(ctx,
|
||||
@@ -454,7 +441,7 @@ func (e *Emoji) fixCacheState(ctx context.Context, emoji *gtsmodel.Emoji) (bool,
|
||||
}
|
||||
|
||||
func (e *Emoji) uncacheRemote(ctx context.Context, after time.Time, emoji *gtsmodel.Emoji) (bool, error) {
|
||||
if !*emoji.Cached {
|
||||
if !emoji.Cached() {
|
||||
// Already uncached.
|
||||
return false, nil
|
||||
}
|
||||
@@ -588,8 +575,11 @@ func (e *Emoji) uncache(ctx context.Context, emoji *gtsmodel.Emoji) error {
|
||||
|
||||
// Update emoji to reflect that we no longer have it cached.
|
||||
log.Debugf(ctx, "marking emoji as uncached: %s", emoji.ID)
|
||||
emoji.Cached = func() *bool { i := false; return &i }()
|
||||
if err := e.state.DB.UpdateEmoji(ctx, emoji, "cached"); err != nil {
|
||||
emoji.ImagePath, emoji.ImageStaticPath = "", ""
|
||||
if err := e.state.DB.UpdateEmoji(ctx, emoji,
|
||||
"image_static_path",
|
||||
"image_path",
|
||||
); err != nil {
|
||||
return gtserror.Newf("error updating emoji: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"code.superseriousbusiness.org/gotosocial/internal/db"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtscontext"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
func copyMap(in map[string]*gtsmodel.Emoji) map[string]*gtsmodel.Emoji {
|
||||
@@ -71,7 +70,9 @@ func (suite *CleanerTestSuite) TestEmojiFixCacheStates() {
|
||||
// rainbow emoji as uncached
|
||||
// so there's something to fix.
|
||||
emojis := copyMap(suite.emojis)
|
||||
emojis["rainbow"].Cached = util.Ptr(false)
|
||||
emoji := emojis["rainbow"]
|
||||
emoji.ImageStaticPath = ""
|
||||
emoji.ImagePath = ""
|
||||
|
||||
suite.testEmojiFixCacheStates(
|
||||
suite.T().Context(),
|
||||
@@ -84,7 +85,9 @@ func (suite *CleanerTestSuite) TestEmojiFixCacheStatesDryRun() {
|
||||
// rainbow emoji as uncached
|
||||
// so there's something to fix.
|
||||
emojis := copyMap(suite.emojis)
|
||||
emojis["rainbow"].Cached = util.Ptr(false)
|
||||
emoji := emojis["rainbow"]
|
||||
emoji.ImageStaticPath = ""
|
||||
emoji.ImagePath = ""
|
||||
|
||||
suite.testEmojiFixCacheStates(
|
||||
gtscontext.SetDryRun(suite.T().Context()),
|
||||
@@ -141,7 +144,7 @@ func (suite *CleanerTestSuite) testEmojiUncacheRemote(ctx context.Context, emoji
|
||||
}
|
||||
|
||||
// Check cache state.
|
||||
if *emoji.Cached {
|
||||
if emoji.Cached() {
|
||||
t.Errorf("emoji %s@%s should have been uncached", emoji.Shortcode, emoji.Domain)
|
||||
}
|
||||
|
||||
@@ -164,7 +167,7 @@ func (suite *CleanerTestSuite) shouldUncacheEmoji(ctx context.Context, emoji *gt
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if emoji.Cached == nil || !*emoji.Cached {
|
||||
if !emoji.Cached() {
|
||||
// Emoji is already uncached.
|
||||
return false, nil
|
||||
}
|
||||
@@ -408,14 +411,12 @@ func (suite *CleanerTestSuite) shouldFixEmojiCacheState(ctx context.Context, emo
|
||||
}
|
||||
|
||||
switch exists := (haveImage && haveStatic); {
|
||||
case emoji.Cached != nil &&
|
||||
*emoji.Cached && !exists:
|
||||
case emoji.Cached() && !exists:
|
||||
// (cached can be nil in tests)
|
||||
// Cached but missing files.
|
||||
return true, nil
|
||||
|
||||
case emoji.Cached != nil &&
|
||||
!*emoji.Cached && exists:
|
||||
case !emoji.Cached() && exists:
|
||||
// (cached can be nil in tests)
|
||||
// Uncached but unexpected files.
|
||||
return true, nil
|
||||
|
||||
+80
-93
@@ -28,12 +28,11 @@ import (
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtscontext"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/id"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/media"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/paging"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/regexes"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/storage"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/uris"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// Media encompasses a set of
|
||||
@@ -145,17 +144,16 @@ func (m *Media) PruneOrphaned(ctx context.Context) (int, error) {
|
||||
// Media is marked as unused if not attached to any status, account or account is suspended.
|
||||
// Context will be checked for `gtscontext.DryRun()` in order to actually perform the action.
|
||||
func (m *Media) PruneUnused(ctx context.Context) (int, error) {
|
||||
var (
|
||||
total int
|
||||
page paging.Page
|
||||
)
|
||||
var total int
|
||||
var page paging.Page
|
||||
|
||||
// Set page select limit.
|
||||
// Setup page w/ select limit.
|
||||
page.Max = paging.MaxID("")
|
||||
page.Limit = selectLimit
|
||||
|
||||
for {
|
||||
// Fetch the next batch of media attachments to next maxID.
|
||||
attachments, err := m.state.DB.GetAttachments(ctx, "", &page)
|
||||
attachments, err := m.state.DB.GetAttachments(ctx, &page)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
return total, gtserror.Newf("error getting attachments: %w", err)
|
||||
}
|
||||
@@ -170,7 +168,7 @@ func (m *Media) PruneUnused(ctx context.Context) (int, error) {
|
||||
|
||||
// Use last ID as the next 'maxID' value.
|
||||
maxID = attachments[len(attachments)-1].ID
|
||||
page.Max = paging.MaxID(maxID)
|
||||
page.Max.Value = maxID
|
||||
|
||||
for _, media := range attachments {
|
||||
// Check / prune unused media attachment.
|
||||
@@ -194,33 +192,42 @@ func (m *Media) PruneUnused(ctx context.Context) (int, error) {
|
||||
// Context will be checked for `gtscontext.DryRun()` in order to actually perform the action.
|
||||
func (m *Media) UncacheRemote(ctx context.Context, olderThan time.Time) (int, error) {
|
||||
var total int
|
||||
var page paging.Page
|
||||
|
||||
// Setup page w/ select limit.
|
||||
page.Max = paging.MaxID("")
|
||||
page.Limit = selectLimit
|
||||
|
||||
// Drop time by a minute to improve search,
|
||||
// (i.e. make it olderThan inclusive search).
|
||||
olderThan = olderThan.Add(-time.Minute)
|
||||
|
||||
// Store recent time.
|
||||
mostRecent := olderThan
|
||||
// Get ULID for 'olderThan' to use as maxID.
|
||||
olderThanID := id.ZeroULIDForTime(olderThan)
|
||||
page.Max.Value = olderThanID
|
||||
|
||||
for {
|
||||
// Fetch the next batch of cached attachments older than last-set time.
|
||||
attachments, err := m.state.DB.GetCachedAttachmentsOlderThan(ctx, olderThan, selectLimit)
|
||||
// Fetch the next batch of cached attachments older than maxID.
|
||||
attachments, err := m.state.DB.GetCachedAttachments(ctx, &page)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
return total, gtserror.Newf("error getting remote attachments: %w", err)
|
||||
}
|
||||
|
||||
// If no attachments / same group is returned, we reached the end.
|
||||
if len(attachments) == 0 ||
|
||||
olderThan.Equal(attachments[len(attachments)-1].CreatedAt) {
|
||||
// Get current max ID.
|
||||
maxID := page.Max.Value
|
||||
|
||||
// If no attachments or the same group is returned, we reached the end.
|
||||
if len(attachments) == 0 || maxID == attachments[len(attachments)-1].ID {
|
||||
break
|
||||
}
|
||||
|
||||
// Use last created-at as the next 'olderThan' value.
|
||||
olderThan = attachments[len(attachments)-1].CreatedAt
|
||||
// Use last ID as the next 'maxID' value.
|
||||
maxID = attachments[len(attachments)-1].ID
|
||||
page.Max.Value = maxID
|
||||
|
||||
for _, media := range attachments {
|
||||
// Check / uncache each remote media attachment.
|
||||
uncached, err := m.uncacheRemote(ctx, mostRecent, media)
|
||||
// Check and try uncache each remote media attachment.
|
||||
uncached, err := m.uncacheRemote(ctx, olderThan, media)
|
||||
if err != nil {
|
||||
return total, err
|
||||
}
|
||||
@@ -239,20 +246,16 @@ func (m *Media) UncacheRemote(ctx context.Context, olderThan time.Time) (int, er
|
||||
// PurgeRemote stubs + uncaches all remote media from the given domain.
|
||||
// Context will be checked for `gtscontext.DryRun()` in order to actually perform the action.
|
||||
func (m *Media) PurgeRemote(ctx context.Context, domain string) (int, error) {
|
||||
var (
|
||||
total int
|
||||
page paging.Page
|
||||
accounts []*gtsmodel.Account
|
||||
err error
|
||||
)
|
||||
var total int
|
||||
var page paging.Page
|
||||
|
||||
// Set page select limit.
|
||||
// Setup page w/ select limit.
|
||||
page.Max = paging.MaxID("")
|
||||
page.Limit = selectLimit
|
||||
|
||||
for {
|
||||
// Get (next) page of accounts
|
||||
// from the target domain.
|
||||
accounts, err = m.state.DB.GetAccounts(
|
||||
// Get (next) page of accounts for domain.
|
||||
accounts, err := m.state.DB.GetAccounts(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
"", // origin
|
||||
"", // status
|
||||
@@ -278,7 +281,7 @@ func (m *Media) PurgeRemote(ctx context.Context, domain string) (int, error) {
|
||||
// Set params for next page.
|
||||
loAcct := accounts[count-1]
|
||||
lo := loAcct.Domain + "/@" + loAcct.Username
|
||||
page.Max = paging.MaxID(lo)
|
||||
page.Max.Value = lo
|
||||
|
||||
// For each account, stub all
|
||||
// that account's attachments.
|
||||
@@ -296,19 +299,16 @@ func (m *Media) PurgeRemote(ctx context.Context, domain string) (int, error) {
|
||||
|
||||
// stubAccountAttachments stubs all attachments belonging to the given accountID.
|
||||
func (m *Media) stubAccountAttachments(ctx context.Context, accountID string) (int, error) {
|
||||
var (
|
||||
total int
|
||||
page paging.Page
|
||||
attachments []*gtsmodel.MediaAttachment
|
||||
err error
|
||||
)
|
||||
var total int
|
||||
var page paging.Page
|
||||
|
||||
// Set page select limit.
|
||||
// Setup page w/ select limit.
|
||||
page.Max = paging.MaxID("")
|
||||
page.Limit = selectLimit
|
||||
|
||||
for {
|
||||
// Get (next) page of attachments from the account.
|
||||
attachments, err = m.state.DB.GetAttachments(
|
||||
// Get (next) page of attachments from db for the account.
|
||||
attachments, err := m.state.DB.GetAttachmentsByAccountID(
|
||||
ctx,
|
||||
accountID,
|
||||
&page,
|
||||
@@ -317,17 +317,19 @@ func (m *Media) stubAccountAttachments(ctx context.Context, accountID string) (i
|
||||
return total, gtserror.Newf("db error getting attachments: %w", err)
|
||||
}
|
||||
|
||||
count := len(attachments)
|
||||
if count == 0 {
|
||||
// We're done.
|
||||
// Get current max ID.
|
||||
maxID := page.Max.Value
|
||||
|
||||
// If no attachments or the same group is returned, we reached the end.
|
||||
if len(attachments) == 0 || maxID == attachments[len(attachments)-1].ID {
|
||||
break
|
||||
}
|
||||
|
||||
// Set params for next page.
|
||||
maxID := attachments[count-1].ID
|
||||
page.Max = paging.MaxID(maxID)
|
||||
// Use last ID as the next 'maxID' value.
|
||||
maxID = attachments[len(attachments)-1].ID
|
||||
page.Max.Value = maxID
|
||||
|
||||
total += count
|
||||
total += len(attachments)
|
||||
if gtscontext.DryRun(ctx) {
|
||||
// If this is a dry run, just increment
|
||||
// the total by the attachment count
|
||||
@@ -336,9 +338,9 @@ func (m *Media) stubAccountAttachments(ctx context.Context, accountID string) (i
|
||||
}
|
||||
|
||||
// Stub each attachment by removing it
|
||||
// from storage if possible, and stubbing
|
||||
// its fields, leaving description, blurhash,
|
||||
// and remoteURL metadata in place.
|
||||
// from storage, and stubbing it fields,
|
||||
// leaving description, blurhash, and
|
||||
// remoteURL metadata in place.
|
||||
for _, a := range attachments {
|
||||
if err := m.stubAttachment(ctx, a); err != nil {
|
||||
return total, err
|
||||
@@ -349,43 +351,26 @@ func (m *Media) stubAccountAttachments(ctx context.Context, accountID string) (i
|
||||
return total, nil
|
||||
}
|
||||
|
||||
// stubAttachment removes stored media + stubs data for one attachment.
|
||||
// stubAttachment removes stored media and stubs all available fields for given attachment.
|
||||
func (m *Media) stubAttachment(ctx context.Context, a *gtsmodel.MediaAttachment) error {
|
||||
if a.File.Path != "" {
|
||||
// Ensure media file at path is deleted from storage.
|
||||
err := m.state.Storage.Delete(ctx, a.File.Path)
|
||||
if err != nil && !storage.IsNotFound(err) {
|
||||
log.Errorf(ctx, "error deleting %s: %v", a.File.Path, err)
|
||||
}
|
||||
|
||||
// Remove any attachment files.
|
||||
if _, err := m.removeFiles(ctx,
|
||||
a.Thumbnail.Path,
|
||||
a.File.Path,
|
||||
); err != nil {
|
||||
log.Error(ctx, err)
|
||||
}
|
||||
|
||||
if a.Thumbnail.Path != "" {
|
||||
// Ensure media thumbnail at path is deleted from storage.
|
||||
err := m.state.Storage.Delete(ctx, a.Thumbnail.Path)
|
||||
if err != nil && !storage.IsNotFound(err) {
|
||||
log.Errorf(ctx, "error deleting %s: %v", a.Thumbnail.Path, err)
|
||||
}
|
||||
}
|
||||
// Unset
|
||||
// fields.
|
||||
a.Stub()
|
||||
|
||||
// Unset all file fields.
|
||||
a.FileMeta.Original = gtsmodel.Original{}
|
||||
a.FileMeta.Small = gtsmodel.Small{}
|
||||
a.File.ContentType = ""
|
||||
a.File.FileSize = 0
|
||||
a.File.Path = ""
|
||||
a.Thumbnail.FileSize = 0
|
||||
a.Thumbnail.ContentType = ""
|
||||
a.Thumbnail.Path = ""
|
||||
a.Thumbnail.URL = ""
|
||||
a.URL = ""
|
||||
|
||||
// Also ensure marked as unknown and finished
|
||||
// processing so gets inserted as placeholder URL.
|
||||
a.Processing = gtsmodel.ProcessingStatusProcessed
|
||||
// Also ensure marked as unknown so
|
||||
// gets inserted as placeholder URL.
|
||||
a.Type = gtsmodel.FileTypeUnknown
|
||||
a.Cached = util.Ptr(false)
|
||||
|
||||
// Update the stubbed attachment.
|
||||
// Update the stubbed attachment in the database.
|
||||
if err := m.state.DB.UpdateAttachment(ctx, a); err != nil {
|
||||
return gtserror.Newf("db error updating attachment: %w", err)
|
||||
}
|
||||
@@ -397,12 +382,11 @@ func (m *Media) stubAttachment(ctx context.Context, a *gtsmodel.MediaAttachment)
|
||||
// Media marked as cached, with any required files missing, will be automatically uncached.
|
||||
// Context will be checked for `gtscontext.DryRun()` in order to actually perform the action.
|
||||
func (m *Media) FixCacheStates(ctx context.Context) (int, error) {
|
||||
var (
|
||||
total int
|
||||
page paging.Page
|
||||
)
|
||||
var total int
|
||||
var page paging.Page
|
||||
|
||||
// Set page select limit.
|
||||
// Setup page w/ select limit.
|
||||
page.Max = paging.MaxID("")
|
||||
page.Limit = selectLimit
|
||||
|
||||
for {
|
||||
@@ -421,7 +405,7 @@ func (m *Media) FixCacheStates(ctx context.Context) (int, error) {
|
||||
|
||||
// Use last ID as the next 'maxID' value.
|
||||
maxID = attachments[len(attachments)-1].ID
|
||||
page.Max = paging.MaxID(maxID)
|
||||
page.Max.Value = maxID
|
||||
|
||||
for _, media := range attachments {
|
||||
// Check / fix required media cache states.
|
||||
@@ -607,13 +591,13 @@ func (m *Media) fixCacheState(ctx context.Context, media *gtsmodel.MediaAttachme
|
||||
return false, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case *media.Cached && !exist:
|
||||
switch cached := media.Cached(); {
|
||||
case cached && !exist:
|
||||
// Mark as uncached if expected files don't exist.
|
||||
l.Debug("cached=true exists=false => uncaching")
|
||||
return true, m.uncache(ctx, media)
|
||||
|
||||
case !*media.Cached && exist:
|
||||
case !cached && exist:
|
||||
// Remove files if we don't expect them to exist.
|
||||
l.Debug("cached=false exists=true => deleting")
|
||||
_, err := m.removeFiles(ctx,
|
||||
@@ -628,7 +612,7 @@ func (m *Media) fixCacheState(ctx context.Context, media *gtsmodel.MediaAttachme
|
||||
}
|
||||
|
||||
func (m *Media) uncacheRemote(ctx context.Context, after time.Time, media *gtsmodel.MediaAttachment) (bool, error) {
|
||||
if !*media.Cached {
|
||||
if !media.Cached() {
|
||||
// Already uncached.
|
||||
return false, nil
|
||||
}
|
||||
@@ -778,8 +762,11 @@ func (m *Media) uncache(ctx context.Context, media *gtsmodel.MediaAttachment) er
|
||||
|
||||
// Update attachment to reflect that we no longer have it cached.
|
||||
log.Debugf(ctx, "marking media attachment as uncached: %s", media.ID)
|
||||
media.Cached = func() *bool { i := false; return &i }()
|
||||
if err := m.state.DB.UpdateAttachment(ctx, media, "cached"); err != nil {
|
||||
media.File.Path, media.Thumbnail.Path = "", ""
|
||||
if err := m.state.DB.UpdateAttachment(ctx, media,
|
||||
"thumbnail_path",
|
||||
"file_path",
|
||||
); err != nil {
|
||||
return gtserror.Newf("error updating media: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -90,10 +90,10 @@ func (suite *MediaTestSuite) TestUncacheRemote() {
|
||||
ctx := suite.T().Context()
|
||||
|
||||
testStatusAttachment := suite.testAttachments["remote_account_1_status_1_attachment_1"]
|
||||
suite.True(*testStatusAttachment.Cached)
|
||||
suite.True(testStatusAttachment.Cached())
|
||||
|
||||
testHeader := suite.testAttachments["remote_account_3_header"]
|
||||
suite.True(*testHeader.Cached)
|
||||
suite.True(testHeader.Cached())
|
||||
|
||||
after := time.Now().Add(-24 * time.Hour)
|
||||
totalUncached, err := suite.cleaner.Media().UncacheRemote(ctx, after)
|
||||
@@ -102,11 +102,11 @@ func (suite *MediaTestSuite) TestUncacheRemote() {
|
||||
|
||||
uncachedAttachment, err := suite.db.GetAttachmentByID(ctx, testStatusAttachment.ID)
|
||||
suite.NoError(err)
|
||||
suite.False(*uncachedAttachment.Cached)
|
||||
suite.False(uncachedAttachment.Cached())
|
||||
|
||||
uncachedAttachment, err = suite.db.GetAttachmentByID(ctx, testHeader.ID)
|
||||
suite.NoError(err)
|
||||
suite.False(*uncachedAttachment.Cached)
|
||||
suite.False(uncachedAttachment.Cached())
|
||||
}
|
||||
|
||||
func (suite *MediaTestSuite) TestPurgeRemote() {
|
||||
@@ -148,10 +148,10 @@ func (suite *MediaTestSuite) TestUncacheRemoteDry() {
|
||||
ctx := suite.T().Context()
|
||||
|
||||
testStatusAttachment := suite.testAttachments["remote_account_1_status_1_attachment_1"]
|
||||
suite.True(*testStatusAttachment.Cached)
|
||||
suite.True(testStatusAttachment.Cached())
|
||||
|
||||
testHeader := suite.testAttachments["remote_account_3_header"]
|
||||
suite.True(*testHeader.Cached)
|
||||
suite.True(testHeader.Cached())
|
||||
|
||||
after := time.Now().Add(-24 * time.Hour)
|
||||
totalUncached, err := suite.cleaner.Media().UncacheRemote(gtscontext.SetDryRun(ctx), after)
|
||||
@@ -160,11 +160,11 @@ func (suite *MediaTestSuite) TestUncacheRemoteDry() {
|
||||
|
||||
uncachedAttachment, err := suite.db.GetAttachmentByID(ctx, testStatusAttachment.ID)
|
||||
suite.NoError(err)
|
||||
suite.True(*uncachedAttachment.Cached)
|
||||
suite.True(uncachedAttachment.Cached())
|
||||
|
||||
uncachedAttachment, err = suite.db.GetAttachmentByID(ctx, testHeader.ID)
|
||||
suite.NoError(err)
|
||||
suite.True(*uncachedAttachment.Cached)
|
||||
suite.True(uncachedAttachment.Cached())
|
||||
}
|
||||
|
||||
func (suite *MediaTestSuite) TestUncacheRemoteTwice() {
|
||||
@@ -223,7 +223,7 @@ func (suite *MediaTestSuite) TestUncacheAndRecache() {
|
||||
suite.NotNil(recachedAttachment)
|
||||
|
||||
// recachedAttachment should be basically the same as the old attachment
|
||||
suite.True(*recachedAttachment.Cached)
|
||||
suite.True(recachedAttachment.Cached())
|
||||
suite.Equal(original.ID, recachedAttachment.ID)
|
||||
suite.Equal(original.File.Path, recachedAttachment.File.Path) // file should be stored in the same place
|
||||
suite.Equal(original.Thumbnail.Path, recachedAttachment.Thumbnail.Path) // as should the thumbnail
|
||||
@@ -244,7 +244,7 @@ func (suite *MediaTestSuite) TestUncacheOneNonExistent() {
|
||||
// Delete this attachment cached on disk
|
||||
media, err := suite.db.GetAttachmentByID(ctx, testStatusAttachment.ID)
|
||||
suite.NoError(err)
|
||||
suite.True(*media.Cached)
|
||||
suite.True(media.Cached())
|
||||
err = suite.storage.Delete(ctx, media.File.Path)
|
||||
suite.NoError(err)
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@ func Validate() error {
|
||||
// Get and check configured max thumb size.
|
||||
switch max := GetMediaThumbMaxPixels(); {
|
||||
case max < minThumb:
|
||||
errf("%s < 32 is not a useable thumbsize", MediaThumbMaxPixelsFlag, max)
|
||||
errf("%s < 32 is not a useable thumbsize", MediaThumbMaxPixelsFlag)
|
||||
case max < minThumbRecc:
|
||||
log.Warnf(nil, "%s smaller than min recommended thumbsize %d", MediaThumbMaxPixelsFlag, minThumbRecc)
|
||||
case max > maxThumbRecc:
|
||||
|
||||
+53
-58
@@ -305,78 +305,73 @@ func (e *emojiDB) GetEmojisBy(ctx context.Context, domain string, includeDisable
|
||||
}
|
||||
|
||||
func (e *emojiDB) GetEmojis(ctx context.Context, page *paging.Page) ([]*gtsmodel.Emoji, error) {
|
||||
maxID := page.GetMax()
|
||||
limit := page.GetLimit()
|
||||
|
||||
emojiIDs := make([]string, 0, limit)
|
||||
|
||||
q := e.db.NewSelect().
|
||||
Table("emojis").
|
||||
Column("id").
|
||||
Order("id DESC")
|
||||
|
||||
if maxID != "" {
|
||||
q = q.Where("id < ?", maxID)
|
||||
}
|
||||
|
||||
if limit != 0 {
|
||||
q = q.Limit(limit)
|
||||
}
|
||||
|
||||
if err := q.Scan(ctx, &emojiIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return e.GetEmojisByIDs(ctx, emojiIDs)
|
||||
return e.getEmojisPagedByID(ctx, nil, page)
|
||||
}
|
||||
|
||||
func (e *emojiDB) GetRemoteEmojis(ctx context.Context, page *paging.Page) ([]*gtsmodel.Emoji, error) {
|
||||
maxID := page.GetMax()
|
||||
limit := page.GetLimit()
|
||||
|
||||
emojiIDs := make([]string, 0, limit)
|
||||
|
||||
q := e.db.NewSelect().
|
||||
Table("emojis").
|
||||
Column("id").
|
||||
Where("domain IS NOT NULL").
|
||||
Order("id DESC")
|
||||
|
||||
if maxID != "" {
|
||||
q = q.Where("id < ?", maxID)
|
||||
}
|
||||
|
||||
if limit != 0 {
|
||||
q = q.Limit(limit)
|
||||
}
|
||||
|
||||
if err := q.Scan(ctx, &emojiIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return e.GetEmojisByIDs(ctx, emojiIDs)
|
||||
return e.getEmojisPagedByID(ctx, func(q *bun.SelectQuery) *bun.SelectQuery {
|
||||
q = q.Where("domain IS NOT NULL")
|
||||
return q
|
||||
}, page)
|
||||
}
|
||||
|
||||
func (e *emojiDB) GetCachedEmojisOlderThan(ctx context.Context, olderThan time.Time, limit int) ([]*gtsmodel.Emoji, error) {
|
||||
var emojiIDs []string
|
||||
func (e *emojiDB) GetCachedEmojis(ctx context.Context, page *paging.Page) ([]*gtsmodel.Emoji, error) {
|
||||
return e.getEmojisPagedByID(ctx, func(q *bun.SelectQuery) *bun.SelectQuery {
|
||||
q = q.Where("domain IS NOT NULL")
|
||||
q = q.Where("image_static_path IS NOT ?", "")
|
||||
q = q.Where("image_path IS NOT ?", "")
|
||||
return q
|
||||
}, page)
|
||||
}
|
||||
|
||||
func (e *emojiDB) getEmojisPagedByID(ctx context.Context, query func(*bun.SelectQuery) *bun.SelectQuery, page *paging.Page) ([]*gtsmodel.Emoji, error) {
|
||||
maxID := page.GetMax()
|
||||
minID := page.GetMin()
|
||||
limit := page.GetLimit()
|
||||
order := page.GetOrder()
|
||||
|
||||
// Pre-allocate slice of dest IDs.
|
||||
ids := make([]string, 0, limit)
|
||||
|
||||
// Start building query.
|
||||
q := e.db.NewSelect().
|
||||
Table("emojis").
|
||||
Column("id").
|
||||
Where("cached = true").
|
||||
Where("domain IS NOT NULL").
|
||||
Where("created_at < ?", olderThan).
|
||||
Order("created_at DESC")
|
||||
Column("id")
|
||||
|
||||
if limit != 0 {
|
||||
q = q.Limit(limit)
|
||||
if query != nil {
|
||||
// Append caller
|
||||
// query details.
|
||||
q = query(q)
|
||||
}
|
||||
|
||||
if err := q.Scan(ctx, &emojiIDs); err != nil {
|
||||
if maxID != "" {
|
||||
// Set a maximum ID boundary if was given.
|
||||
q = q.Where("? < ?", bun.Ident("id"), maxID)
|
||||
}
|
||||
|
||||
if minID != "" {
|
||||
// Set a minimum ID boundary if was given.
|
||||
q = q.Where("? > ?", bun.Ident("id"), minID)
|
||||
}
|
||||
|
||||
// Set query ordering.
|
||||
if order.Ascending() {
|
||||
q = q.OrderExpr("? ASC", bun.Ident("id"))
|
||||
} else /* i.e. descending */ {
|
||||
q = q.OrderExpr("? DESC", bun.Ident("id"))
|
||||
}
|
||||
|
||||
// A limit should always
|
||||
// be supplied for this.
|
||||
q = q.Limit(limit)
|
||||
|
||||
// Finally, perform query into IDs slice.
|
||||
if err := q.Scan(ctx, &ids); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return e.GetEmojisByIDs(ctx, emojiIDs)
|
||||
// Fetch emoji from DB with IDs.
|
||||
return e.GetEmojisByIDs(ctx, ids)
|
||||
}
|
||||
|
||||
func (e *emojiDB) GetUseableEmojis(ctx context.Context) ([]*gtsmodel.Emoji, error) {
|
||||
|
||||
+54
-60
@@ -21,7 +21,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"code.superseriousbusiness.org/gopkg/xslices"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/db"
|
||||
@@ -224,82 +223,77 @@ func (m *mediaDB) DeleteAttachment(ctx context.Context, id string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *mediaDB) GetAttachments(ctx context.Context, accountID string, page *paging.Page) ([]*gtsmodel.MediaAttachment, error) {
|
||||
maxID := page.GetMax()
|
||||
limit := page.GetLimit()
|
||||
func (m *mediaDB) GetAttachments(ctx context.Context, page *paging.Page) ([]*gtsmodel.MediaAttachment, error) {
|
||||
return m.getAttachmentsPagedByID(ctx, nil, page)
|
||||
}
|
||||
|
||||
attachmentIDs := make([]string, 0, limit)
|
||||
|
||||
q := m.db.NewSelect().
|
||||
Table("media_attachments").
|
||||
Column("id").
|
||||
Order("id DESC")
|
||||
|
||||
if accountID != "" {
|
||||
q = q.Where("? = ?", bun.Ident("account_id"), accountID)
|
||||
}
|
||||
|
||||
if maxID != "" {
|
||||
q = q.Where("? < ?", bun.Ident("id"), maxID)
|
||||
}
|
||||
|
||||
if limit != 0 {
|
||||
q = q.Limit(limit)
|
||||
}
|
||||
|
||||
if err := q.Scan(ctx, &attachmentIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m.GetAttachmentsByIDs(ctx, attachmentIDs)
|
||||
func (m *mediaDB) GetAttachmentsByAccountID(ctx context.Context, accountID string, page *paging.Page) ([]*gtsmodel.MediaAttachment, error) {
|
||||
return m.getAttachmentsPagedByID(ctx, func(q *bun.SelectQuery) *bun.SelectQuery {
|
||||
return q.Where("? = ?", bun.Ident("account_id"), accountID)
|
||||
}, page)
|
||||
}
|
||||
|
||||
func (m *mediaDB) GetRemoteAttachments(ctx context.Context, page *paging.Page) ([]*gtsmodel.MediaAttachment, error) {
|
||||
return m.getAttachmentsPagedByID(ctx, func(q *bun.SelectQuery) *bun.SelectQuery {
|
||||
return q.Where("remote_url IS NOT NULL")
|
||||
}, page)
|
||||
}
|
||||
|
||||
func (m *mediaDB) GetCachedAttachments(ctx context.Context, page *paging.Page) ([]*gtsmodel.MediaAttachment, error) {
|
||||
return m.getAttachmentsPagedByID(ctx, func(q *bun.SelectQuery) *bun.SelectQuery {
|
||||
q = q.Where("remote_url IS NOT NULL")
|
||||
q = q.Where("file_path IS NOT ?", "")
|
||||
q = q.Where("thumbnail_path IS NOT ?", "")
|
||||
return q
|
||||
}, page)
|
||||
}
|
||||
|
||||
func (m *mediaDB) getAttachmentsPagedByID(ctx context.Context, query func(*bun.SelectQuery) *bun.SelectQuery, page *paging.Page) ([]*gtsmodel.MediaAttachment, error) {
|
||||
maxID := page.GetMax()
|
||||
minID := page.GetMin()
|
||||
limit := page.GetLimit()
|
||||
order := page.GetOrder()
|
||||
|
||||
attachmentIDs := make([]string, 0, limit)
|
||||
// Pre-allocate slice of dest IDs.
|
||||
ids := make([]string, 0, limit)
|
||||
|
||||
// Start building query.
|
||||
q := m.db.NewSelect().
|
||||
Table("media_attachments").
|
||||
Column("id").
|
||||
Where("remote_url IS NOT NULL").
|
||||
Order("id DESC")
|
||||
Column("id")
|
||||
|
||||
if query != nil {
|
||||
// Append caller
|
||||
// query details.
|
||||
q = query(q)
|
||||
}
|
||||
|
||||
if maxID != "" {
|
||||
q = q.Where("id < ?", maxID)
|
||||
// Set a maximum ID boundary if was given.
|
||||
q = q.Where("? < ?", bun.Ident("id"), maxID)
|
||||
}
|
||||
|
||||
if limit != 0 {
|
||||
q = q.Limit(limit)
|
||||
if minID != "" {
|
||||
// Set a minimum ID boundary if was given.
|
||||
q = q.Where("? > ?", bun.Ident("id"), minID)
|
||||
}
|
||||
|
||||
if err := q.Scan(ctx, &attachmentIDs); err != nil {
|
||||
// Set query ordering.
|
||||
if order.Ascending() {
|
||||
q = q.OrderExpr("? ASC", bun.Ident("id"))
|
||||
} else /* i.e. descending */ {
|
||||
q = q.OrderExpr("? DESC", bun.Ident("id"))
|
||||
}
|
||||
|
||||
// A limit should always
|
||||
// be supplied for this.
|
||||
q = q.Limit(limit)
|
||||
|
||||
// Finally, perform query into IDs slice.
|
||||
if err := q.Scan(ctx, &ids); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m.GetAttachmentsByIDs(ctx, attachmentIDs)
|
||||
}
|
||||
|
||||
func (m *mediaDB) GetCachedAttachmentsOlderThan(ctx context.Context, olderThan time.Time, limit int) ([]*gtsmodel.MediaAttachment, error) {
|
||||
attachmentIDs := make([]string, 0, limit)
|
||||
|
||||
q := m.db.
|
||||
NewSelect().
|
||||
Table("media_attachments").
|
||||
Column("id").
|
||||
Where("cached = true").
|
||||
Where("remote_url IS NOT NULL").
|
||||
Where("created_at < ?", olderThan).
|
||||
Order("created_at DESC")
|
||||
|
||||
if limit != 0 {
|
||||
q = q.Limit(limit)
|
||||
}
|
||||
|
||||
if err := q.Scan(ctx, &attachmentIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m.GetAttachmentsByIDs(ctx, attachmentIDs)
|
||||
// Fetch media from DB with given IDs.
|
||||
return m.GetAttachmentsByIDs(ctx, ids)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.superseriousbusiness.org/gotosocial/internal/id"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
@@ -35,16 +36,9 @@ func (suite *MediaTestSuite) TestGetAttachmentByID() {
|
||||
suite.NotNil(attachment)
|
||||
}
|
||||
|
||||
func (suite *MediaTestSuite) TestGetOlder() {
|
||||
attachments, err := suite.db.GetCachedAttachmentsOlderThan(suite.T().Context(), time.Now(), 20)
|
||||
suite.NoError(err)
|
||||
suite.Len(attachments, 3)
|
||||
}
|
||||
|
||||
func (suite *MediaTestSuite) TestGetCachedAttachmentsOlderThan() {
|
||||
ctx := suite.T().Context()
|
||||
|
||||
attachments, err := suite.db.GetCachedAttachmentsOlderThan(ctx, time.Now(), 20)
|
||||
olderThanID := id.ZeroULIDForTime(time.Now())
|
||||
attachments, err := suite.db.GetCachedAttachments(suite.T().Context(), toPage(olderThanID, "", "", 20))
|
||||
suite.NoError(err)
|
||||
suite.Len(attachments, 3)
|
||||
}
|
||||
|
||||
@@ -31,8 +31,8 @@ func init() {
|
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
|
||||
// Generate new Account.Indexable column definition from bun.
|
||||
statusType := reflect.TypeOf((*gtsmodel.Account)(nil))
|
||||
colDef, err := getBunColumnDef(tx, statusType, "Indexable")
|
||||
accountType := reflect.TypeOf((*gtsmodel.Account)(nil))
|
||||
colDef, err := getBunColumnDef(tx, accountType, "Indexable")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
// 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"
|
||||
|
||||
newmodel "code.superseriousbusiness.org/gotosocial/internal/db/bundb/migrations/20251208134945_media_cleanup/newmodel"
|
||||
oldmodel "code.superseriousbusiness.org/gotosocial/internal/db/bundb/migrations/20251208134945_media_cleanup/oldmodel"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
|
||||
"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 {
|
||||
|
||||
// Add new error columns to the database.
|
||||
for model, field := range map[any]string{
|
||||
(*newmodel.MediaAttachment)(nil): "Error",
|
||||
(*newmodel.Emoji)(nil): "Error",
|
||||
} {
|
||||
if err := addColumn(ctx, tx, model, field); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Drop old media cleanup index that relies on below dropped columns.
|
||||
if err := dropIndex(ctx, tx, "media_attachments_cleanup_idx"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create new media cleanup index.
|
||||
if err := createIndex(ctx, tx,
|
||||
"media_attachments_cleanup_idx",
|
||||
"media_attachments",
|
||||
"?, ?", bun.Ident("file_path"), bun.Ident("thumbnail_path"),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Unset all file paths for media
|
||||
// attachments that are uncached,
|
||||
// to match new caching strategy.
|
||||
if _, err := tx.NewUpdate().
|
||||
Table("media_attachments").
|
||||
Where("? IS NULL OR ? = false", bun.Ident("cached"), bun.Ident("cached")).
|
||||
Set("? = ?", bun.Ident("thumbnail_path"), "").
|
||||
Set("? = ?", bun.Ident("file_path"), "").
|
||||
Exec(ctx); err != nil {
|
||||
return gtserror.Newf("error updating uncached media: %w", err)
|
||||
}
|
||||
|
||||
// Unset all file paths for emoji
|
||||
// attachments that are uncached,
|
||||
// to match new caching strategy.
|
||||
if _, err := tx.NewUpdate().
|
||||
Table("emojis").
|
||||
Where("? IS NULL OR ? = false", bun.Ident("cached"), bun.Ident("cached")).
|
||||
Set("? = ?", bun.Ident("image_static_path"), "").
|
||||
Set("? = ?", bun.Ident("image_path"), "").
|
||||
Exec(ctx); err != nil {
|
||||
return gtserror.Newf("error updating uncached emojis: %w", err)
|
||||
}
|
||||
|
||||
// Drop (now) unused columns from database.
|
||||
for model, fields := range map[any][]string{
|
||||
(*oldmodel.MediaAttachment)(nil): {"Cached", "Processing"},
|
||||
(*oldmodel.Emoji)(nil): {"Cached"},
|
||||
} {
|
||||
for _, field := range fields {
|
||||
if err := dropColumn(ctx, tx, model, field); 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)
|
||||
}
|
||||
}
|
||||
+8
-3
@@ -15,8 +15,13 @@
|
||||
// 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 main
|
||||
package gtsmodel
|
||||
|
||||
import "code.superseriousbusiness.org/gotosocial/internal/id"
|
||||
// smallint is the largest size supported
|
||||
// by a PostgreSQL SMALLINT, since an SQLite
|
||||
// SMALLINT is actually variable in size.
|
||||
type smallint int16
|
||||
|
||||
func main() { println(id.NewULID()) }
|
||||
// enumType is the type we (at least, should) use
|
||||
// for database enum types, as smallest int size.
|
||||
type enumType smallint
|
||||
@@ -0,0 +1,45 @@
|
||||
// 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 `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||
Shortcode string `bun:",nullzero,notnull,unique:domainshortcode"` // String shortcode for this emoji -- the part that's between colons. This should be a-zA-Z_ eg., 'blob_hug' 'purple_heart' 'Gay_Otter' Must be unique with domain.
|
||||
Domain string `bun:",nullzero,unique:domainshortcode"` // Origin domain of this emoji, eg 'example.org', 'queer.party'. empty string for local emojis.
|
||||
ImageRemoteURL string `bun:",nullzero"` // Where can 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.
|
||||
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:",notnull"` // Path 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:",notnull"` // MIME content type of the emoji image
|
||||
ImageStaticContentType string `bun:",notnull"` // MIME content type of the static version of the emoji image.
|
||||
ImageFileSize int `bun:",notnull"` // Size 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.
|
||||
Error MediaErrorDetails `bun:",nullzero,notnull,default:0"` // Details about any error encountered downloading file
|
||||
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'
|
||||
VisibleInPicker *bool `bun:",nullzero,notnull,default:true"` // Is this emoji visible in the admin emoji picker?
|
||||
CategoryID string `bun:"type:CHAR(26),nullzero"` // ID of the category this emoji belongs to.
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
// MediaAttachment represents a user-uploaded media attachment: an image/video/audio/gif that is
|
||||
// somewhere in storage and that can be retrieved and served by the router.
|
||||
type MediaAttachment struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
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
|
||||
RemoteURL string `bun:",nullzero"` // Where can the attachment be retrieved on a remote server (empty for local media)
|
||||
Type FileType `bun:",notnull,default:0"` // Type of file (image/gifv/audio/video/unknown)
|
||||
Error MediaErrorDetails `bun:",nullzero,notnull,default:0"` // Details about any error encountered downloading 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
|
||||
Description string `bun:""` // Description of the attachment (for screenreaders)
|
||||
ScheduledStatusID string `bun:"type:CHAR(26),nullzero"` // To which scheduled status does this attachment belong
|
||||
Blurhash string `bun:",nullzero"` // What is the generated blurhash of this attachment
|
||||
File File `bun:",embed:file_,notnull,nullzero"` // metadata for the whole file
|
||||
Thumbnail Thumbnail `bun:",embed:thumbnail_,notnull,nullzero"` // small image thumbnail derived from a larger image, video, or audio file.
|
||||
Avatar *bool `bun:",nullzero,notnull,default:false"` // Is this attachment being used as an avatar?
|
||||
Header *bool `bun:",nullzero,notnull,default:false"` // Is this attachment being used as a header?
|
||||
}
|
||||
|
||||
// File refers to the metadata for the whole file.
|
||||
type File struct {
|
||||
Path string `bun:",notnull"` // Path of the file in storage.
|
||||
ContentType string `bun:",notnull"` // MIME content type of the file.
|
||||
FileSize int `bun:",notnull"` // File size in bytes
|
||||
}
|
||||
|
||||
// Thumbnail refers to a small image thumbnail derived from a larger image, video, or audio file.
|
||||
type Thumbnail struct {
|
||||
Path string `bun:",notnull"` // Path of the file in storage.
|
||||
ContentType string `bun:",notnull"` // MIME content type of the file.
|
||||
FileSize int `bun:",notnull"` // File size in bytes
|
||||
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)
|
||||
}
|
||||
|
||||
// FileType refers to the file
|
||||
// type of the media attaachment.
|
||||
type FileType enumType
|
||||
|
||||
// FileMeta describes metadata about the actual contents of the file.
|
||||
type FileMeta struct {
|
||||
Original Original `bun:"embed:original_"`
|
||||
Small Small `bun:"embed:small_"`
|
||||
Focus Focus `bun:"embed:focus_"`
|
||||
}
|
||||
|
||||
// Small can be used for a thumbnail of any media type
|
||||
type Small struct {
|
||||
Width int // width in pixels
|
||||
Height int // height in pixels
|
||||
Size int // size in pixels (width * height)
|
||||
Aspect float32 // aspect ratio (width / height)
|
||||
}
|
||||
|
||||
// Original can be used for original metadata for any media type
|
||||
type Original struct {
|
||||
Width int // width in pixels
|
||||
Height int // height in pixels
|
||||
Size int // size in pixels (width * height)
|
||||
Aspect float32 // aspect ratio (width / height)
|
||||
Duration *float32 // video-specific: duration of the video in seconds
|
||||
Framerate *float32 // video-specific: fps
|
||||
Bitrate *uint64 // video-specific: bitrate
|
||||
}
|
||||
|
||||
// Focus describes the 'center' of the image for display purposes.
|
||||
// X and Y should each be between -1 and 1
|
||||
type Focus struct {
|
||||
X float32
|
||||
Y float32
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// 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
|
||||
|
||||
// MediaErrorDetails stores basic error details about
|
||||
// why a piece of media may not have been downloaded.
|
||||
// It contains a 16bit MediaErrorType, and the remaining
|
||||
// 16bits may contain optional extra error details.
|
||||
type MediaErrorDetails uint32
|
||||
|
||||
// MediaErrorType describes a broad error type for why
|
||||
// one or more media files may not have been downloaded.
|
||||
type MediaErrorType uint16
|
||||
@@ -0,0 +1,45 @@
|
||||
// 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 `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||
Shortcode string `bun:",nullzero,notnull,unique:domainshortcode"` // String shortcode for this emoji -- the part that's between colons. This should be a-zA-Z_ eg., 'blob_hug' 'purple_heart' 'Gay_Otter' Must be unique with domain.
|
||||
Domain string `bun:",nullzero,unique:domainshortcode"` // Origin domain of this emoji, eg 'example.org', 'queer.party'. empty string for local emojis.
|
||||
ImageRemoteURL string `bun:",nullzero"` // Where can 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.
|
||||
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:",notnull"` // Path 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:",notnull"` // MIME content type of the emoji image
|
||||
ImageStaticContentType string `bun:",notnull"` // MIME content type of the static version of the emoji image.
|
||||
ImageFileSize int `bun:",notnull"` // Size 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.
|
||||
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'
|
||||
VisibleInPicker *bool `bun:",nullzero,notnull,default:true"` // Is this emoji visible in the admin emoji picker?
|
||||
CategoryID string `bun:"type:CHAR(26),nullzero"` // ID of the category this emoji belongs to.
|
||||
Cached *bool `bun:",nullzero,notnull,default:false"` // whether emoji is cached in locally in gotosocial storage.
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
// MediaAttachment represents a user-uploaded media attachment: an image/video/audio/gif that is
|
||||
// somewhere in storage and that can be retrieved and served by the router.
|
||||
type MediaAttachment struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
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
|
||||
RemoteURL string `bun:",nullzero"` // Where can the attachment be retrieved on a remote server (empty for local media)
|
||||
Type FileType `bun:",notnull,default:0"` // Type of file (image/gifv/audio/video/unknown)
|
||||
FileMeta FileMeta `bun:",embed:,notnull"` // Metadata about the file
|
||||
AccountID string `bun:"type:CHAR(26),nullzero,notnull"` // To which account does this attachment belong
|
||||
Description string `bun:""` // Description of the attachment (for screenreaders)
|
||||
ScheduledStatusID string `bun:"type:CHAR(26),nullzero"` // To which scheduled status does this attachment belong
|
||||
Blurhash string `bun:",nullzero"` // What is the generated blurhash of this attachment
|
||||
Processing ProcessingStatus `bun:",notnull,default:2"` // What is the processing status of this attachment
|
||||
File File `bun:",embed:file_,notnull,nullzero"` // metadata for the whole file
|
||||
Thumbnail Thumbnail `bun:",embed:thumbnail_,notnull,nullzero"` // small image thumbnail derived from a larger image, video, or audio file.
|
||||
Avatar *bool `bun:",nullzero,notnull,default:false"` // Is this attachment being used as an avatar?
|
||||
Header *bool `bun:",nullzero,notnull,default:false"` // Is this attachment being used as a header?
|
||||
Cached *bool `bun:",nullzero,notnull,default:false"` // Is this attachment currently cached by our instance?
|
||||
}
|
||||
|
||||
// File refers to the metadata for the whole file
|
||||
type File struct {
|
||||
Path string `bun:",notnull"` // Path of the file in storage.
|
||||
ContentType string `bun:",notnull"` // MIME content type of the file.
|
||||
FileSize int `bun:",notnull"` // File size in bytes
|
||||
}
|
||||
|
||||
// Thumbnail refers to a small image thumbnail derived from a larger image, video, or audio file.
|
||||
type Thumbnail struct {
|
||||
Path string `bun:",notnull"` // Path of the file in storage.
|
||||
ContentType string `bun:",notnull"` // MIME content type of the file.
|
||||
FileSize int `bun:",notnull"` // File size in bytes
|
||||
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)
|
||||
}
|
||||
|
||||
// ProcessingStatus refers to how far along in the processing stage the attachment is.
|
||||
type ProcessingStatus int
|
||||
|
||||
// FileType refers to the file
|
||||
// type of the media attaachment.
|
||||
type FileType int
|
||||
|
||||
// FileMeta describes metadata about the actual contents of the file.
|
||||
type FileMeta struct {
|
||||
Original Original `bun:"embed:original_"`
|
||||
Small Small `bun:"embed:small_"`
|
||||
Focus Focus `bun:"embed:focus_"`
|
||||
}
|
||||
|
||||
// Small can be used for a thumbnail of any media type
|
||||
type Small struct {
|
||||
Width int // width in pixels
|
||||
Height int // height in pixels
|
||||
Size int // size in pixels (width * height)
|
||||
Aspect float32 // aspect ratio (width / height)
|
||||
}
|
||||
|
||||
// Original can be used for original metadata for any media type
|
||||
type Original struct {
|
||||
Width int // width in pixels
|
||||
Height int // height in pixels
|
||||
Size int // size in pixels (width * height)
|
||||
Aspect float32 // aspect ratio (width / height)
|
||||
Duration *float32 // video-specific: duration of the video in seconds
|
||||
Framerate *float32 // video-specific: fps
|
||||
Bitrate *uint64 // video-specific: bitrate
|
||||
}
|
||||
|
||||
// Focus describes the 'center' of the image for display purposes.
|
||||
// X and Y should each be between -1 and 1
|
||||
type Focus struct {
|
||||
X float32
|
||||
Y float32
|
||||
}
|
||||
@@ -237,6 +237,88 @@ func convertEnums[OldType ~string, NewType ~int16](
|
||||
return nil
|
||||
}
|
||||
|
||||
func addColumn(ctx context.Context, db bun.IDB, model any, fieldName string) error {
|
||||
rtype := reflect.TypeOf(model)
|
||||
|
||||
// Get bun field information for this field on model.
|
||||
field, table, err := getModelField(db, rtype, fieldName)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error getting bun field %s.%s: %w", rtype, field, err)
|
||||
}
|
||||
|
||||
// Generate bun column definition for model field.
|
||||
colDef, err := getBunColumnDef(db, rtype, fieldName)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error getting bun column definition for %T.%s: %w", rtype, fieldName, err)
|
||||
}
|
||||
|
||||
log.Infof(ctx, "adding column '%s.%s'", table.Name, field.Name)
|
||||
|
||||
// Add column to database.
|
||||
_, err = db.NewAddColumn().
|
||||
Table(table.Name).
|
||||
ColumnExpr(colDef).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error adding column '%s.%s': %w", table.Name, field.Name, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func dropColumn(ctx context.Context, db bun.IDB, model any, fieldName string) error {
|
||||
rtype := reflect.TypeOf(model)
|
||||
|
||||
// Get bun field information for this field on model.
|
||||
field, table, err := getModelField(db, rtype, fieldName)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error getting bun field %s.%s: %w", rtype, field, err)
|
||||
}
|
||||
|
||||
log.Infof(ctx, "dropping column '%s.%s'", table.Name, field.Name)
|
||||
|
||||
// Attempt to drop this column.
|
||||
_, err = db.NewDropColumn().
|
||||
Table(table.Name).
|
||||
Column(field.Name).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error dropping column '%s.%s': %w", table.Name, field.Name, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createIndex(ctx context.Context, db bun.IDB, indexName, tableName, colExpr string, colArgs ...any) error {
|
||||
log.Infof(ctx, "creating index '%s' on '%s'", indexName, tableName)
|
||||
|
||||
// Attempt to create this index.
|
||||
_, err := db.NewCreateIndex().
|
||||
Table(tableName).
|
||||
Index(indexName).
|
||||
ColumnExpr(colExpr, colArgs...).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error creating index '%s': %w", indexName, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func dropIndex(ctx context.Context, db bun.IDB, indexName string) error {
|
||||
log.Infof(ctx, "dropping index '%s'", indexName)
|
||||
|
||||
// Attempt to drop this index.
|
||||
_, err := db.NewDropIndex().
|
||||
Index(indexName).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error dropping index '%s': %w", indexName, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getBunColumnDef generates a column definition string for the SQL table represented by
|
||||
// Go type, with the SQL column represented by the given Go field name. This ensures when
|
||||
// adding a new column for table by migration that it will end up as bun would create it.
|
||||
|
||||
@@ -19,7 +19,6 @@ package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/paging"
|
||||
@@ -50,11 +49,11 @@ type Emoji interface {
|
||||
// GetEmojis fetches all emojis with IDs less than 'maxID', up to a maximum of 'limit' emojis.
|
||||
GetEmojis(ctx context.Context, page *paging.Page) ([]*gtsmodel.Emoji, error)
|
||||
|
||||
// GetRemoteEmojis fetches all remote emojis with IDs less than 'maxID', up to a maximum of 'limit' emojis.
|
||||
// GetRemoteEmojis fetches emojis with a non-empty domain, with given paging parameters.
|
||||
GetRemoteEmojis(ctx context.Context, page *paging.Page) ([]*gtsmodel.Emoji, error)
|
||||
|
||||
// GetCachedEmojisOlderThan fetches all cached remote emojis with 'updated_at' greater than 'olderThan', up to a maximum of 'limit' emojis.
|
||||
GetCachedEmojisOlderThan(ctx context.Context, olderThan time.Time, limit int) ([]*gtsmodel.Emoji, error)
|
||||
// GetCachedEmojis fetches cached emojis with a non-empty domain, with given paging parameters.
|
||||
GetCachedEmojis(ctx context.Context, page *paging.Page) ([]*gtsmodel.Emoji, error)
|
||||
|
||||
// GetEmojisBy gets emojis based on given parameters. Useful for admin actions.
|
||||
GetEmojisBy(ctx context.Context, domain string, includeDisabled bool, includeEnabled bool, shortcode string, maxShortcodeDomain string, minShortcodeDomain string, limit int) ([]*gtsmodel.Emoji, error)
|
||||
|
||||
@@ -19,7 +19,6 @@ package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/paging"
|
||||
@@ -42,14 +41,15 @@ type Media interface {
|
||||
// DeleteAttachment deletes the attachment with given ID from the database.
|
||||
DeleteAttachment(ctx context.Context, id string) error
|
||||
|
||||
// GetAttachments fetches media attachments up to a given max ID, and at most limit.
|
||||
// AccountID is optional and can be provided to specify only attachments from given account.
|
||||
GetAttachments(ctx context.Context, accountID string, page *paging.Page) ([]*gtsmodel.MediaAttachment, error)
|
||||
// GetAttachments fetches media attachments, with given paging parameters.
|
||||
GetAttachments(ctx context.Context, page *paging.Page) ([]*gtsmodel.MediaAttachment, error)
|
||||
|
||||
// GetRemoteAttachments fetches media attachments with a non-empty domain, up to a given max ID, and at most limit.
|
||||
// GetAttachmentsByAccountID fetches media attachments by account ID, with given paging parameters.
|
||||
GetAttachmentsByAccountID(ctx context.Context, accountID string, page *paging.Page) ([]*gtsmodel.MediaAttachment, error)
|
||||
|
||||
// GetRemoteAttachments fetches media attachments with a non-empty domain, with given paging parameters.
|
||||
GetRemoteAttachments(ctx context.Context, page *paging.Page) ([]*gtsmodel.MediaAttachment, error)
|
||||
|
||||
// GetCachedAttachmentsOlderThan gets limit n remote attachments (including avatars and headers) older than
|
||||
// the given time. These will be returned in order of attachment.created_at descending (i.e. newest to oldest).
|
||||
GetCachedAttachmentsOlderThan(ctx context.Context, olderThan time.Time, limit int) ([]*gtsmodel.MediaAttachment, error)
|
||||
// GetCachedAttachments fetches cached media attachments with a non-empty domain, with given paging parameters.
|
||||
GetCachedAttachments(ctx context.Context, page *paging.Page) ([]*gtsmodel.MediaAttachment, error)
|
||||
}
|
||||
|
||||
@@ -806,13 +806,23 @@ func (d *Dereferencer) enrichAccount(
|
||||
latestAcc.FetchedAt = now
|
||||
latestAcc.UpdatedAt = now
|
||||
|
||||
// Check whether there's any limits in
|
||||
// place for this domain / subdomain.
|
||||
// Check if there's any limits in place for (sub)domain.
|
||||
limit, err := d.state.DB.MatchDomainLimit(ctx, uri.Host)
|
||||
if err != nil {
|
||||
return nil, nil, gtserror.Newf("error matching domain limit: %w", err)
|
||||
}
|
||||
rejectMedia := limit.MediaReject()
|
||||
|
||||
// If domain media is limited, set reject reason,
|
||||
// this gets passed to media fetching functions
|
||||
// and prevents download of attached account media.
|
||||
var rejectReason *gtsmodel.MediaErrorDetails
|
||||
if limit.MediaReject() {
|
||||
rejectReason = new(gtsmodel.MediaErrorDetails)
|
||||
*rejectReason = gtsmodel.NewMediaErrorDetails(
|
||||
gtsmodel.MediaErrorTypePolicy,
|
||||
gtsmodel.MediaErrorTypePolicy_Domain,
|
||||
)
|
||||
}
|
||||
|
||||
// Ensure the account's avatar media
|
||||
// is populated (if appropriate), passing
|
||||
@@ -823,7 +833,7 @@ func (d *Dereferencer) enrichAccount(
|
||||
account,
|
||||
latestAcc,
|
||||
apubAcc,
|
||||
rejectMedia,
|
||||
rejectReason,
|
||||
); err != nil {
|
||||
log.Errorf(ctx, "error fetching remote avatar for account %s: %v", uri, err)
|
||||
}
|
||||
@@ -837,7 +847,7 @@ func (d *Dereferencer) enrichAccount(
|
||||
account,
|
||||
latestAcc,
|
||||
apubAcc,
|
||||
rejectMedia,
|
||||
rejectReason,
|
||||
); err != nil {
|
||||
log.Errorf(ctx, "error fetching remote header for account %s: %v", uri, err)
|
||||
}
|
||||
@@ -848,7 +858,7 @@ func (d *Dereferencer) enrichAccount(
|
||||
ctx,
|
||||
account,
|
||||
latestAcc,
|
||||
rejectMedia,
|
||||
rejectReason,
|
||||
); err != nil {
|
||||
log.Errorf(ctx, "error fetching remote emojis for account %s: %v", uri, err)
|
||||
}
|
||||
@@ -887,7 +897,7 @@ func (d *Dereferencer) fetchAccountAvatar(
|
||||
existingAcc *gtsmodel.Account,
|
||||
latestAcc *gtsmodel.Account,
|
||||
apubAcc ap.Accountable,
|
||||
rejectMedia bool,
|
||||
rejectReason *gtsmodel.MediaErrorDetails, // optional reason to reject media with
|
||||
) error {
|
||||
latestAvatarURL := latestAcc.AvatarRemoteURL
|
||||
if latestAvatarURL == "" {
|
||||
@@ -896,18 +906,19 @@ func (d *Dereferencer) fetchAccountAvatar(
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check for image description of avatar.
|
||||
// Check for image description of account header avatar.
|
||||
avatarDescription := ap.ExtractIconDescription(apubAcc)
|
||||
|
||||
// If account has a avatar set, and the avatar media
|
||||
// has the same URL as the latest up-to-date URL,
|
||||
// just ensure we have it cached (if appropriate).
|
||||
if existingAcc.AvatarSet() && existingAcc.AvatarRemoteURL == latestAvatarURL {
|
||||
if existingAcc.AvatarSet() &&
|
||||
existingAcc.AvatarRemoteURL == latestAvatarURL {
|
||||
|
||||
// Avatar URL from existing account is up to date,
|
||||
// so attachment ID will be up to date as well.
|
||||
//
|
||||
// Get this avatar media attachment from db.
|
||||
// Get this avatar media attachment from database.
|
||||
existing, err := d.state.DB.GetAttachmentByID(ctx,
|
||||
existingAcc.AvatarMediaAttachmentID,
|
||||
)
|
||||
@@ -916,15 +927,12 @@ func (d *Dereferencer) fetchAccountAvatar(
|
||||
}
|
||||
|
||||
if existing != nil {
|
||||
|
||||
// Prepare to update info if necessary.
|
||||
var info media.AdditionalMediaInfo
|
||||
|
||||
// If set, pass rejectMedia flag to
|
||||
// derefencer to skip downloading.
|
||||
if rejectMedia {
|
||||
info.RejectMedia = &rejectMedia
|
||||
}
|
||||
// Pass reject reason ptr, which
|
||||
// will skip downloading if set.
|
||||
info.RejectReason = rejectReason
|
||||
|
||||
// If description has changed,
|
||||
// ensure this gets updated.
|
||||
@@ -962,28 +970,22 @@ func (d *Dereferencer) fetchAccountAvatar(
|
||||
return nil
|
||||
}
|
||||
|
||||
// If existing was nil, then the avatar
|
||||
// attachment got removed for some reason,
|
||||
// so fall through to getting it from
|
||||
// scratch amd setting a new ID, but log
|
||||
// this as it's a bit strange.
|
||||
log.Info(ctx,
|
||||
"avatar %s was not found in db, refetching it from scratch",
|
||||
// If existing was nil, then the attachment got removed for some reason,
|
||||
// fall to fetching from scratch with a new ID, but log as it is strange.
|
||||
log.Warnf(ctx, "%s not found in db, refetching from scratch",
|
||||
existingAcc.AvatarMediaAttachmentID,
|
||||
)
|
||||
}
|
||||
|
||||
// Prepare media info.
|
||||
// Prepare additional media info.
|
||||
info := media.AdditionalMediaInfo{
|
||||
Avatar: util.Ptr(true),
|
||||
RemoteURL: &latestAvatarURL,
|
||||
Description: &avatarDescription,
|
||||
}
|
||||
|
||||
// If set, pass rejectMedia flag to
|
||||
// derefencer to skip downloading.
|
||||
if rejectMedia {
|
||||
info.RejectMedia = &rejectMedia
|
||||
// Pass reject reason ptr, which
|
||||
// will skip downloading if set.
|
||||
RejectReason: rejectReason,
|
||||
}
|
||||
|
||||
// Fetch newly changed avatar.
|
||||
@@ -997,16 +999,15 @@ func (d *Dereferencer) fetchAccountAvatar(
|
||||
switch {
|
||||
case err == nil:
|
||||
// No problem,
|
||||
// loaded it fine.
|
||||
// loaded fine.
|
||||
|
||||
case attachment == nil:
|
||||
// Fatal error occurred during
|
||||
// loading, can't do anything with this.
|
||||
return gtserror.Newf("error loading attachment %s: %w", latestAvatarURL, err)
|
||||
// Fatal error during loading, can't do anything further.
|
||||
return gtserror.Newf("error loading attachment %s: %w",
|
||||
latestAvatarURL, err)
|
||||
|
||||
default:
|
||||
// Non-fatal error occurred
|
||||
// during loading, still use it.
|
||||
// Non-fatal error during loading, can still use it.
|
||||
log.Warnf(ctx, "partially loaded attachment: %v", err)
|
||||
}
|
||||
|
||||
@@ -1024,7 +1025,7 @@ func (d *Dereferencer) fetchAccountHeader(
|
||||
existingAcc *gtsmodel.Account,
|
||||
latestAcc *gtsmodel.Account,
|
||||
apubAcc ap.Accountable,
|
||||
rejectMedia bool,
|
||||
rejectReason *gtsmodel.MediaErrorDetails, // optional reason to reject media with
|
||||
) error {
|
||||
latestHeaderURL := latestAcc.HeaderRemoteURL
|
||||
if latestHeaderURL == "" {
|
||||
@@ -1033,18 +1034,19 @@ func (d *Dereferencer) fetchAccountHeader(
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check for image description of header.
|
||||
// Check for image description of account header image.
|
||||
headerDescription := ap.ExtractImageDescription(apubAcc)
|
||||
|
||||
// If account has a header set, and the header media
|
||||
// has the same URL as the latest up-to-date URL,
|
||||
// just ensure we have it cached (if appropriate).
|
||||
if existingAcc.HeaderSet() && existingAcc.HeaderRemoteURL == latestHeaderURL {
|
||||
if existingAcc.HeaderSet() &&
|
||||
existingAcc.HeaderRemoteURL == latestHeaderURL {
|
||||
|
||||
// Header URL from existing account is up to date,
|
||||
// so attachment ID will be up to date as well.
|
||||
//
|
||||
// Get this header media attachment from db.
|
||||
// Get this header media attachment from database.
|
||||
existing, err := d.state.DB.GetAttachmentByID(ctx,
|
||||
existingAcc.HeaderMediaAttachmentID,
|
||||
)
|
||||
@@ -1053,15 +1055,12 @@ func (d *Dereferencer) fetchAccountHeader(
|
||||
}
|
||||
|
||||
if existing != nil {
|
||||
|
||||
// Prepare to update info if necessary.
|
||||
var info media.AdditionalMediaInfo
|
||||
|
||||
// If set, pass rejectMedia flag to
|
||||
// derefencer to skip downloading.
|
||||
if rejectMedia {
|
||||
info.RejectMedia = &rejectMedia
|
||||
}
|
||||
// Pass reject reason ptr, which
|
||||
// will skip downloading if set.
|
||||
info.RejectReason = rejectReason
|
||||
|
||||
// If description has changed,
|
||||
// ensure this gets updated.
|
||||
@@ -1099,28 +1098,22 @@ func (d *Dereferencer) fetchAccountHeader(
|
||||
return nil
|
||||
}
|
||||
|
||||
// If existing was nil, then the header
|
||||
// attachment got removed for some reason,
|
||||
// so fall through to getting it from
|
||||
// scratch amd setting a new ID, but log
|
||||
// this as it's a bit strange.
|
||||
log.Info(ctx,
|
||||
"header %s was not found in db, refetching it from scratch",
|
||||
// If existing was nil, then the attachment got removed for some reason,
|
||||
// fall to fetching from scratch with a new ID, but log as it is strange.
|
||||
log.Warnf(ctx, "%s not found in db, refetching from scratch",
|
||||
existingAcc.HeaderMediaAttachmentID,
|
||||
)
|
||||
}
|
||||
|
||||
// Prepare media info.
|
||||
// Prepare additional media info.
|
||||
info := media.AdditionalMediaInfo{
|
||||
Header: util.Ptr(true),
|
||||
RemoteURL: &latestHeaderURL,
|
||||
Description: &headerDescription,
|
||||
}
|
||||
|
||||
// If set, pass rejectMedia flag to
|
||||
// derefencer to skip downloading.
|
||||
if rejectMedia {
|
||||
info.RejectMedia = &rejectMedia
|
||||
// Pass reject reason ptr, which
|
||||
// will skip downloading if set.
|
||||
RejectReason: rejectReason,
|
||||
}
|
||||
|
||||
// Fetch newly changed header.
|
||||
@@ -1134,16 +1127,15 @@ func (d *Dereferencer) fetchAccountHeader(
|
||||
switch {
|
||||
case err == nil:
|
||||
// No problem,
|
||||
// loaded it fine.
|
||||
// loaded fine.
|
||||
|
||||
case attachment == nil:
|
||||
// Fatal error occurred during
|
||||
// loading, can't do anything with this.
|
||||
return gtserror.Newf("error loading attachment %s: %w", latestHeaderURL, err)
|
||||
// Fatal error during loading, can't do anything further.
|
||||
return gtserror.Newf("error loading attachment %s: %w",
|
||||
latestHeaderURL, err)
|
||||
|
||||
default:
|
||||
// Non-fatal error occurred
|
||||
// during loading, still use it.
|
||||
// Non-fatal error during loading, can still use it.
|
||||
log.Warnf(ctx, "partially loaded attachment: %v", err)
|
||||
}
|
||||
|
||||
@@ -1159,13 +1151,13 @@ func (d *Dereferencer) fetchAccountEmojis(
|
||||
ctx context.Context,
|
||||
existing *gtsmodel.Account,
|
||||
account *gtsmodel.Account,
|
||||
rejectMedia bool,
|
||||
rejectReason *gtsmodel.MediaErrorDetails, // optional reason to reject media with
|
||||
) error {
|
||||
// Fetch the updated emojis for the account.
|
||||
emojis, changed, err := d.fetchEmojis(ctx,
|
||||
existing.Emojis,
|
||||
account.Emojis,
|
||||
rejectMedia, // rejectMedia
|
||||
rejectReason,
|
||||
)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error fetching emojis: %w", err)
|
||||
|
||||
@@ -511,10 +511,15 @@ func (suite *AccountTestSuite) TestDereferenceRemoteAccountWithAvatarDescription
|
||||
remotePerson,
|
||||
nil,
|
||||
)
|
||||
|
||||
suite.NoError(err)
|
||||
suite.NotNil(apAcc)
|
||||
suite.Equal(updatedAcc.AvatarMediaAttachment.Description, description)
|
||||
|
||||
// our account media fetches are
|
||||
// async, so wait until updated.
|
||||
testrig.WaitFor(func() bool {
|
||||
media, _ := suite.state.DB.GetAttachmentByID(ctx, updatedAcc.AvatarMediaAttachmentID)
|
||||
return media != nil && media.Description == description
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccountTestSuite(t *testing.T) {
|
||||
|
||||
@@ -252,7 +252,7 @@ func (d *Dereferencer) RecacheEmoji(
|
||||
return emoji, nil
|
||||
}
|
||||
|
||||
if *emoji.Cached {
|
||||
if emoji.Cached() {
|
||||
// Already cached.
|
||||
return emoji, nil
|
||||
}
|
||||
@@ -279,7 +279,7 @@ func (d *Dereferencer) RecacheEmoji(
|
||||
return nil, nil, gtserror.Newf("error fetching emoji from db: %w", err)
|
||||
}
|
||||
|
||||
if emoji != nil && *emoji.Cached {
|
||||
if emoji != nil && emoji.Cached() {
|
||||
// This was *just* cached.
|
||||
return nil, emoji, nil
|
||||
}
|
||||
@@ -414,7 +414,7 @@ func (d *Dereferencer) fetchEmojis(
|
||||
ctx context.Context,
|
||||
existing []*gtsmodel.Emoji,
|
||||
emojis []*gtsmodel.Emoji, // newly dereferenced
|
||||
rejectMedia bool,
|
||||
rejectReason *gtsmodel.MediaErrorDetails, // optional reason to reject media with
|
||||
) (
|
||||
[]*gtsmodel.Emoji,
|
||||
bool, // any changes?
|
||||
@@ -423,13 +423,6 @@ func (d *Dereferencer) fetchEmojis(
|
||||
// Track any changes.
|
||||
changed := false
|
||||
|
||||
// If we're rejecting media from this
|
||||
// domain, set this once outside the loop.
|
||||
var rejectMediaPtr *bool
|
||||
if rejectMedia {
|
||||
rejectMediaPtr = &rejectMedia
|
||||
}
|
||||
|
||||
for i, placeholder := range emojis {
|
||||
// Look for an existing emoji with shortcode + domain.
|
||||
existing, ok := getEmojiByShortcodeDomain(existing,
|
||||
@@ -447,12 +440,14 @@ func (d *Dereferencer) fetchEmojis(
|
||||
URI: &placeholder.URI,
|
||||
ImageRemoteURL: &placeholder.ImageRemoteURL,
|
||||
ImageStaticRemoteURL: &placeholder.ImageStaticRemoteURL,
|
||||
// Pass rejectMedia ptr to derefencer
|
||||
// to skip downloading if necessary.
|
||||
RejectMedia: rejectMediaPtr,
|
||||
|
||||
// Pass reject reason ptr, which
|
||||
// will skip downloading if set.
|
||||
RejectReason: rejectReason,
|
||||
}
|
||||
|
||||
// Ensure that the existing emoji model is up-to-date and cached.
|
||||
// Ensure that the existing emoji
|
||||
// model is up-to-date and cached.
|
||||
existing, err := d.RefreshEmoji(
|
||||
ctx,
|
||||
existing,
|
||||
@@ -483,12 +478,10 @@ func (d *Dereferencer) fetchEmojis(
|
||||
URI: &placeholder.URI,
|
||||
ImageRemoteURL: &placeholder.ImageRemoteURL,
|
||||
ImageStaticRemoteURL: &placeholder.ImageStaticRemoteURL,
|
||||
}
|
||||
|
||||
// If set, pass rejectMedia flag to
|
||||
// derefencer to skip downloading.
|
||||
if rejectMedia {
|
||||
info.RejectMedia = &rejectMedia
|
||||
// Pass reject reason ptr, which
|
||||
// will skip downloading if set.
|
||||
RejectReason: rejectReason,
|
||||
}
|
||||
|
||||
// Fetch this newly added emoji,
|
||||
|
||||
@@ -142,8 +142,15 @@ func (d *Dereferencer) RefreshMedia(
|
||||
force = true
|
||||
}
|
||||
|
||||
// Check if needs updating.
|
||||
if *attach.Cached && !force {
|
||||
switch {
|
||||
case force:
|
||||
// Unset any previous error
|
||||
// to force a dereference.
|
||||
attach.Error = 0
|
||||
|
||||
case attach.Cached():
|
||||
// Return early, is already
|
||||
// cached and no force flag.
|
||||
return attach, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -585,13 +585,23 @@ func (d *Dereferencer) enrichStatus(
|
||||
return nil, nil, gtserror.Newf("error populating tags for status %s: %w", uri, err)
|
||||
}
|
||||
|
||||
// Check whether there's any limits in
|
||||
// place for this domain / subdomain.
|
||||
// Check if there's any limits in place for (sub)domain.
|
||||
limit, err := d.state.DB.MatchDomainLimit(ctx, uri.Host)
|
||||
if err != nil {
|
||||
return nil, nil, gtserror.Newf("error matching domain limit: %w", err)
|
||||
}
|
||||
rejectMedia := limit.MediaReject()
|
||||
|
||||
// If domain media is limited, set reject reason,
|
||||
// this gets passed to media fetching functions
|
||||
// and prevents download of attached status media.
|
||||
var rejectReason *gtsmodel.MediaErrorDetails
|
||||
if limit.MediaReject() {
|
||||
rejectReason = new(gtsmodel.MediaErrorDetails)
|
||||
*rejectReason = gtsmodel.NewMediaErrorDetails(
|
||||
gtsmodel.MediaErrorTypePolicy,
|
||||
gtsmodel.MediaErrorTypePolicy_Domain,
|
||||
)
|
||||
}
|
||||
|
||||
// Populate media attachments associated with status,
|
||||
// passing in existing status to reuse old where possible
|
||||
@@ -600,7 +610,7 @@ func (d *Dereferencer) enrichStatus(
|
||||
requestUser,
|
||||
status,
|
||||
latestStatus,
|
||||
rejectMedia,
|
||||
rejectReason,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, gtserror.Newf("error populating attachments for status %s: %w", uri, err)
|
||||
@@ -612,7 +622,7 @@ func (d *Dereferencer) enrichStatus(
|
||||
emojiChanged, err := d.fetchStatusEmojis(ctx,
|
||||
status,
|
||||
latestStatus,
|
||||
rejectMedia,
|
||||
rejectReason,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, gtserror.Newf("error populating emojis for status %s: %w", uri, err)
|
||||
@@ -821,32 +831,24 @@ func (d *Dereferencer) fetchStatusAttachments(
|
||||
requestUser string,
|
||||
existing *gtsmodel.Status,
|
||||
status *gtsmodel.Status,
|
||||
rejectMedia bool,
|
||||
rejectReason *gtsmodel.MediaErrorDetails, // optional reason to reject media with
|
||||
) (
|
||||
changed bool,
|
||||
err error,
|
||||
) {
|
||||
|
||||
// Allocate new slice to take the yet-to-be fetched attachment IDs.
|
||||
status.AttachmentIDs = make([]string, len(status.Attachments))
|
||||
|
||||
// If we're rejecting media from this
|
||||
// domain, set this once outside the loop.
|
||||
var rejectMediaPtr *bool
|
||||
if rejectMedia {
|
||||
rejectMediaPtr = &rejectMedia
|
||||
}
|
||||
|
||||
for i := range status.Attachments {
|
||||
|
||||
placeholder := status.Attachments[i]
|
||||
|
||||
// Look for existing media attachment with remote URL first.
|
||||
existing, ok := existing.GetAttachmentByRemoteURL(placeholder.RemoteURL)
|
||||
if ok && existing.ID != "" {
|
||||
info := media.AdditionalMediaInfo{
|
||||
// Pass rejectMedia ptr to derefencer
|
||||
// to skip downloading if necessary.
|
||||
RejectMedia: rejectMediaPtr,
|
||||
// Pass reject reason ptr, which
|
||||
// will skip downloading if set.
|
||||
RejectReason: rejectReason,
|
||||
}
|
||||
|
||||
// Look for any difference in stored media description.
|
||||
@@ -900,7 +902,10 @@ func (d *Dereferencer) fetchStatusAttachments(
|
||||
Blurhash: &placeholder.Blurhash,
|
||||
FocusX: &placeholder.FileMeta.Focus.X,
|
||||
FocusY: &placeholder.FileMeta.Focus.Y,
|
||||
RejectMedia: &rejectMedia,
|
||||
|
||||
// Pass reject reason ptr, which
|
||||
// will skip downloading if set.
|
||||
RejectReason: rejectReason,
|
||||
},
|
||||
false, // async
|
||||
)
|
||||
@@ -940,7 +945,7 @@ func (d *Dereferencer) fetchStatusEmojis(
|
||||
ctx context.Context,
|
||||
existing *gtsmodel.Status,
|
||||
status *gtsmodel.Status,
|
||||
rejectMedia bool,
|
||||
rejectReason *gtsmodel.MediaErrorDetails, // optional reason to reject media with
|
||||
) (
|
||||
changed bool,
|
||||
err error,
|
||||
@@ -950,7 +955,7 @@ func (d *Dereferencer) fetchStatusEmojis(
|
||||
emojis, changed, err := d.fetchEmojis(ctx,
|
||||
existing.Emojis,
|
||||
status.Emojis,
|
||||
rejectMedia,
|
||||
rejectReason,
|
||||
)
|
||||
if err != nil {
|
||||
return changed, gtserror.Newf("error fetching emojis: %w", err)
|
||||
|
||||
@@ -164,8 +164,15 @@ func (l *keyedList[T]) put(key string, value T) {
|
||||
func (l *keyedList[T]) delete(key string) {
|
||||
for i, kv := range *l {
|
||||
if kv.k == key {
|
||||
copy((*l)[:i], (*l)[i+1:])
|
||||
(*l) = (*l)[:len(*l)-1]
|
||||
if len := len(*l); len > 1 {
|
||||
// Reslice and clear elem.
|
||||
copy((*l)[:i], (*l)[i+1:])
|
||||
clear((*l)[len-1:])
|
||||
(*l) = (*l)[:len-1]
|
||||
} else if cap(*l) > 64 {
|
||||
// Drop slice.
|
||||
(*l) = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
+23
-10
@@ -33,18 +33,30 @@ func Newf(msgf string, args ...any) error {
|
||||
return newfAt(3, msgf, args...)
|
||||
}
|
||||
|
||||
// NewfAt returns a new error with message with given
|
||||
// calldepth+1. Provide calldepth 2 to prepend only the
|
||||
// name of the current containing function, 3 to prepend
|
||||
// the name of the function containing *that* function,
|
||||
// and so on...
|
||||
//
|
||||
// This function is just exposed for if you want to
|
||||
// create an error wrapping function that maintains
|
||||
// the function prefix of *your wrapper's caller*.
|
||||
// In all other cases simply use New().
|
||||
func NewAt(calldepth int, msg string) error {
|
||||
return newAt(calldepth+1, msg)
|
||||
}
|
||||
|
||||
// NewfAt returns a new formatted error with the given
|
||||
// calldepth+1, useful when you want to wrap an error
|
||||
// from within an anonymous function or utility function,
|
||||
// but preserve the name in the error of the wrapping
|
||||
// function that did the calling.
|
||||
// calldepth+1. Provide calldepth 2 to prepend only the
|
||||
// name of the current containing function, 3 to prepend
|
||||
// the name of the function containing *that* function,
|
||||
// and so on...
|
||||
//
|
||||
// Provide calldepth 2 to prepend only the name of the
|
||||
// current containing function, 3 to prepend the name
|
||||
// of the function containing *that* function, and so on.
|
||||
//
|
||||
// This function is just exposed for dry-dick optimization
|
||||
// purposes. Most callers should just call Newf instead.
|
||||
// This function is just exposed for if you want to
|
||||
// create an error wrapping function that maintains
|
||||
// the function prefix of *your wrapper's caller*.
|
||||
// In all other cases simply use Newf().
|
||||
func NewfAt(calldepth int, msgf string, args ...any) error {
|
||||
return newfAt(calldepth+1, msgf, args...)
|
||||
}
|
||||
@@ -54,6 +66,7 @@ func NewfAt(calldepth int, msgf string, args ...any) error {
|
||||
// will also wrap the returned error using WithStatusCode() and
|
||||
// will include the caller function name as a prefix.
|
||||
func NewFromResponse(rsp *http.Response) error {
|
||||
|
||||
// Build error with message without
|
||||
// using "fmt", as chances are this will
|
||||
// be used in a hot code path and we
|
||||
|
||||
+40
-21
@@ -22,27 +22,27 @@ 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 `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||
Shortcode string `bun:",nullzero,notnull,unique:domainshortcode"` // String shortcode for this emoji -- the part that's between colons. This should be a-zA-Z_ eg., 'blob_hug' 'purple_heart' 'Gay_Otter' Must be unique with domain.
|
||||
Domain string `bun:",nullzero,unique:domainshortcode"` // Origin domain of this emoji, eg 'example.org', 'queer.party'. empty string for local emojis.
|
||||
ImageRemoteURL string `bun:",nullzero"` // Where can 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.
|
||||
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:",notnull"` // Path 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:",notnull"` // MIME content type of the emoji image
|
||||
ImageStaticContentType string `bun:",notnull"` // MIME content type of the static version of the emoji image.
|
||||
ImageFileSize int `bun:",notnull"` // Size 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.
|
||||
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'
|
||||
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?
|
||||
CategoryID string `bun:"type:CHAR(26),nullzero"` // ID of the category this emoji belongs to.
|
||||
Cached *bool `bun:",nullzero,notnull,default:false"` // whether emoji is cached in locally in gotosocial storage.
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||
Shortcode string `bun:",nullzero,notnull,unique:domainshortcode"` // String shortcode for this emoji -- the part that's between colons. This should be a-zA-Z_ eg., 'blob_hug' 'purple_heart' 'Gay_Otter' Must be unique with domain.
|
||||
Domain string `bun:",nullzero,unique:domainshortcode"` // Origin domain of this emoji, eg 'example.org', 'queer.party'. empty string for local emojis.
|
||||
ImageRemoteURL string `bun:",nullzero"` // Where can 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.
|
||||
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:",notnull"` // Path 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:",notnull"` // MIME content type of the emoji image
|
||||
ImageStaticContentType string `bun:",notnull"` // MIME content type of the static version of the emoji image.
|
||||
ImageFileSize int `bun:",notnull"` // Size 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.
|
||||
Error MediaErrorDetails `bun:",notnull,default:0"` // Details about any error encountered downloading file
|
||||
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'
|
||||
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?
|
||||
CategoryID string `bun:"type:CHAR(26),nullzero"` // ID of the category this emoji belongs to.
|
||||
}
|
||||
|
||||
// IsLocal returns true if the emoji is
|
||||
@@ -56,3 +56,22 @@ func (e *Emoji) IsLocal() bool {
|
||||
func (e *Emoji) ShortcodeDomain() string {
|
||||
return e.Shortcode + "@" + e.Domain
|
||||
}
|
||||
|
||||
// Cached returns whether Emoji is cached locally.
|
||||
func (e *Emoji) Cached() bool {
|
||||
return e.ImagePath != "" &&
|
||||
e.ImageStaticPath != ""
|
||||
}
|
||||
|
||||
// Stub will reset all non-essential emoji
|
||||
// fields, leaving it in the bare uncached state.
|
||||
func (e *Emoji) Stub() {
|
||||
e.ImageStaticContentType = ""
|
||||
e.ImageStaticFileSize = 0
|
||||
e.ImageStaticPath = ""
|
||||
e.ImageStaticURL = ""
|
||||
e.ImageContentType = ""
|
||||
e.ImageFileSize = 0
|
||||
e.ImagePath = ""
|
||||
e.ImageURL = ""
|
||||
}
|
||||
|
||||
@@ -24,23 +24,22 @@ import (
|
||||
// MediaAttachment represents a user-uploaded media attachment: an image/video/audio/gif that is
|
||||
// somewhere in storage and that can be retrieved and served by the router.
|
||||
type MediaAttachment struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
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
|
||||
RemoteURL string `bun:",nullzero"` // Where can the attachment be retrieved on a remote server (empty for local media)
|
||||
Type FileType `bun:",notnull,default:0"` // Type of file (image/gifv/audio/video/unknown)
|
||||
FileMeta FileMeta `bun:",embed:,notnull"` // Metadata about the file
|
||||
AccountID string `bun:"type:CHAR(26),nullzero,notnull"` // To which account does this attachment belong
|
||||
Description string `bun:""` // Description of the attachment (for screenreaders)
|
||||
ScheduledStatusID string `bun:"type:CHAR(26),nullzero"` // To which scheduled status does this attachment belong
|
||||
Blurhash string `bun:",nullzero"` // What is the generated blurhash of this attachment
|
||||
Processing ProcessingStatus `bun:",notnull,default:2"` // What is the processing status of this attachment
|
||||
File File `bun:",embed:file_,notnull,nullzero"` // metadata for the whole file
|
||||
Thumbnail Thumbnail `bun:",embed:thumbnail_,notnull,nullzero"` // small image thumbnail derived from a larger image, video, or audio file.
|
||||
Avatar *bool `bun:",nullzero,notnull,default:false"` // Is this attachment being used as an avatar?
|
||||
Header *bool `bun:",nullzero,notnull,default:false"` // Is this attachment being used as a header?
|
||||
Cached *bool `bun:",nullzero,notnull,default:false"` // Is this attachment currently cached by our instance?
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
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
|
||||
RemoteURL string `bun:",nullzero"` // Where can the attachment be retrieved on a remote server (empty for local media)
|
||||
Type FileType `bun:",notnull,default:0"` // Type of file (image/gifv/audio/video/unknown)
|
||||
Error MediaErrorDetails `bun:",notnull,default:0"` // Details about any error encountered downloading 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
|
||||
Description string `bun:""` // Description of the attachment (for screenreaders)
|
||||
ScheduledStatusID string `bun:"type:CHAR(26),nullzero"` // To which scheduled status does this attachment belong
|
||||
Blurhash string `bun:",nullzero"` // What is the generated blurhash of this attachment
|
||||
File File `bun:",embed:file_,notnull,nullzero"` // metadata for the whole file
|
||||
Thumbnail Thumbnail `bun:",embed:thumbnail_,notnull,nullzero"` // small image thumbnail derived from a larger image, video, or audio file.
|
||||
Avatar *bool `bun:",nullzero,notnull,default:false"` // Is this attachment being used as an avatar?
|
||||
Header *bool `bun:",nullzero,notnull,default:false"` // Is this attachment being used as a header?
|
||||
}
|
||||
|
||||
// IsLocal returns whether media attachment is local.
|
||||
@@ -53,13 +52,37 @@ func (m *MediaAttachment) IsRemote() bool {
|
||||
return m.RemoteURL != ""
|
||||
}
|
||||
|
||||
// File refers to the metadata for the whole file
|
||||
// Cached returns whether MediaAttachment is cached locally.
|
||||
func (m *MediaAttachment) Cached() bool {
|
||||
return m.File.Cached() && m.Thumbnail.Cached()
|
||||
}
|
||||
|
||||
// Stub will reset all non-essential attachment
|
||||
// fields, leaving it in the bare never-downloaded state.
|
||||
func (m *MediaAttachment) Stub() {
|
||||
|
||||
// we specifically don't stub
|
||||
// out filemeta or URLs, as it
|
||||
// can be useful if it's going
|
||||
// to be later recached.
|
||||
m.File.ContentType = ""
|
||||
m.File.FileSize = 0
|
||||
m.File.Path = ""
|
||||
m.Thumbnail.FileSize = 0
|
||||
m.Thumbnail.ContentType = ""
|
||||
m.Thumbnail.Path = ""
|
||||
}
|
||||
|
||||
// File refers to the metadata for the whole file.
|
||||
type File struct {
|
||||
Path string `bun:",notnull"` // Path of the file in storage.
|
||||
ContentType string `bun:",notnull"` // MIME content type of the file.
|
||||
FileSize int `bun:",notnull"` // File size in bytes
|
||||
}
|
||||
|
||||
// Cached returns whether this File is cached locally.
|
||||
func (f File) Cached() bool { return f.Path != "" }
|
||||
|
||||
// Thumbnail refers to a small image thumbnail derived from a larger image, video, or audio file.
|
||||
type Thumbnail struct {
|
||||
Path string `bun:",notnull"` // Path of the file in storage.
|
||||
@@ -69,20 +92,12 @@ type Thumbnail struct {
|
||||
RemoteURL string `bun:",nullzero"` // What is the remote URL of the thumbnail (empty for local media)
|
||||
}
|
||||
|
||||
// ProcessingStatus refers to how far along in the processing stage the attachment is.
|
||||
type ProcessingStatus int
|
||||
|
||||
// MediaAttachment processing states.
|
||||
const (
|
||||
ProcessingStatusReceived ProcessingStatus = 0 // ProcessingStatusReceived indicates the attachment has been received and is awaiting processing. No thumbnail available yet.
|
||||
ProcessingStatusProcessing ProcessingStatus = 1 // ProcessingStatusProcessing indicates the attachment is currently being processed. Thumbnail is available but full media is not.
|
||||
ProcessingStatusProcessed ProcessingStatus = 2 // ProcessingStatusProcessed indicates the attachment has been fully processed and is ready to be served.
|
||||
ProcessingStatusError ProcessingStatus = 666 // ProcessingStatusError indicates something went wrong processing the attachment and it won't be tried again--these can be deleted.
|
||||
)
|
||||
// Cached returns whether this Thumbnail is cached locally.
|
||||
func (t Thumbnail) Cached() bool { return t.Path != "" }
|
||||
|
||||
// FileType refers to the file
|
||||
// type of the media attaachment.
|
||||
type FileType int
|
||||
type FileType enumType
|
||||
|
||||
const (
|
||||
// MediaAttachment file types.
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
// 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 (
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// MediaErrorDetails stores basic error details about
|
||||
// why a piece of media may not have been downloaded.
|
||||
// It contains a 16bit MediaErrorType, and the remaining
|
||||
// 16bits may contain optional extra error details.
|
||||
type MediaErrorDetails uint32
|
||||
|
||||
// MediaErrorType describes a broad error type for why
|
||||
// one or more media files may not have been downloaded.
|
||||
type MediaErrorType uint16
|
||||
|
||||
const (
|
||||
// MediaErrorTypeNone: no error returned.
|
||||
MediaErrorTypeNone MediaErrorType = 0
|
||||
|
||||
// MediaErrorTypePolicy: file(s) not downloaded due to configured policy.
|
||||
MediaErrorTypePolicy MediaErrorType = 1
|
||||
MediaErrorTypePolicy_Size uint16 = 1 // nolint:revive
|
||||
MediaErrorTypePolicy_Domain uint16 = 2 // nolint:revive
|
||||
|
||||
// MediaErrorTypeInterrupt: file(s) not downloaded due to interrupt (i.e. context errors).
|
||||
MediaErrorTypeInterrupt MediaErrorType = 2
|
||||
|
||||
// MediaErrorTypeHTTP: file(s) not downloaded due to HTTP response error.
|
||||
// (the remaining 16bits of MediaErrorDetails store status code response)
|
||||
MediaErrorTypeHTTP MediaErrorType = 3
|
||||
|
||||
// MediaErrorTypeNetwork: file(s) not downloaded due to network issue.
|
||||
MediaErrorTypeNetwork MediaErrorType = 4
|
||||
MediaErrorTypeNetwork_Timeout uint16 = 1 // nolint:revive
|
||||
MediaErrorTypeNetwork_DNS uint16 = 2 // nolint:revive
|
||||
|
||||
// MediaErrorTypeCodec: file(s) not downloaded due to a codec issue.
|
||||
MediaErrorTypeCodec MediaErrorType = 5
|
||||
MediaErrorTypeCodec_Unsupported uint16 = 1 // nolint:revive
|
||||
|
||||
// MediaErrorTypeUnknown: file(s) not downloaded due to unclassified error.
|
||||
MediaErrorTypeUnknown MediaErrorType = 6
|
||||
)
|
||||
|
||||
// NewMediaErrorDetails returns a new MediaErrorDetails encapsulating MediaErrorType and details (if any).
|
||||
func NewMediaErrorDetails(errType MediaErrorType, details uint16) MediaErrorDetails {
|
||||
var d MediaErrorDetails
|
||||
d.Set(errType, details)
|
||||
return d
|
||||
}
|
||||
|
||||
// Set will set the receiving MediaErrorDetails with MediaErrorType and extra details (if any).
|
||||
func (d *MediaErrorDetails) Set(errType MediaErrorType, details uint16) {
|
||||
(*d) = MediaErrorDetails(packu16s(uint16(errType), details))
|
||||
}
|
||||
|
||||
// Type returns embedded MediaErrorType within details.
|
||||
func (d MediaErrorDetails) Type() MediaErrorType {
|
||||
const bits = 16
|
||||
return MediaErrorType(uint16(d >> bits)) // nolint:gosec
|
||||
}
|
||||
|
||||
// Details returns extra details related to Type(), if any are set.
|
||||
func (d MediaErrorDetails) Details() uint16 {
|
||||
const bits = 16
|
||||
const mask = (1 << bits) - 1
|
||||
return uint16(d & mask) // nolint:gosec
|
||||
}
|
||||
|
||||
// SupportsRetry returns whether error supports a re-attempt
|
||||
// to cache the media, i.e. due to it likely being transient.
|
||||
func (d MediaErrorDetails) SupportsRetry() bool {
|
||||
switch d.Type() {
|
||||
|
||||
// Either no error was encountered and it was
|
||||
// later uncached, or the original fetch was
|
||||
// interrupted by cancelled request etc.
|
||||
case MediaErrorTypeNone,
|
||||
MediaErrorTypeInterrupt:
|
||||
return true
|
||||
|
||||
// All policy and media codec /
|
||||
// processing errors are permanent.
|
||||
case MediaErrorTypePolicy,
|
||||
MediaErrorTypeCodec:
|
||||
return false
|
||||
|
||||
// On timeout errors we can retry, others
|
||||
// are more likely to be permanent.
|
||||
case MediaErrorTypeNetwork:
|
||||
return d.Details() == MediaErrorTypeNetwork_Timeout
|
||||
|
||||
// HTTP response code errors
|
||||
// can be handled granularly
|
||||
// depending on situation.
|
||||
case MediaErrorTypeHTTP:
|
||||
switch code := d.Details(); {
|
||||
|
||||
// 400-403 type errors (e.g. auth, forbidden, bad request)
|
||||
// *can* be transient e.g. due to bugs. Others in the 4xx
|
||||
// range are generally more permanent (e.g. not found).
|
||||
case code >= 404:
|
||||
return false
|
||||
|
||||
// More likely to be
|
||||
// a temporary error.
|
||||
case code >= 500:
|
||||
return true
|
||||
|
||||
// All else
|
||||
// we deny.
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Default to yes.
|
||||
return true
|
||||
}
|
||||
|
||||
// String returns a frontend API (and log) string describing error details.
|
||||
func (d MediaErrorDetails) String() string {
|
||||
switch errType := d.Type(); errType {
|
||||
case MediaErrorTypeNone:
|
||||
return "none"
|
||||
case MediaErrorTypePolicy:
|
||||
switch d.Details() {
|
||||
case MediaErrorTypePolicy_Size:
|
||||
return "file size limit reached"
|
||||
case MediaErrorTypePolicy_Domain:
|
||||
return "domain media policy"
|
||||
default:
|
||||
return "configuration policy"
|
||||
}
|
||||
case MediaErrorTypeInterrupt:
|
||||
return "connection interrupted"
|
||||
case MediaErrorTypeHTTP:
|
||||
status := int(d.Details())
|
||||
return "http response (status code: " + strconv.Itoa(status) +
|
||||
" " + http.StatusText(status) + ")"
|
||||
case MediaErrorTypeNetwork:
|
||||
switch d.Details() {
|
||||
case MediaErrorTypeNetwork_Timeout:
|
||||
return "network timeout"
|
||||
default:
|
||||
return "network error"
|
||||
}
|
||||
case MediaErrorTypeCodec:
|
||||
switch d.Details() {
|
||||
case MediaErrorTypeCodec_Unsupported:
|
||||
return "unsupported media type"
|
||||
default:
|
||||
return "media processing error"
|
||||
}
|
||||
default:
|
||||
return "unclassified"
|
||||
}
|
||||
}
|
||||
|
||||
func packu16s(u1, u2 uint16) uint32 {
|
||||
const bits = 16
|
||||
const mask = (1 << bits) - 1
|
||||
return uint32(u1)<<bits | uint32(u2)&mask
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// 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_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var mediaErrorDetailsTests = []struct {
|
||||
p gtsmodel.MediaErrorDetails
|
||||
u1 gtsmodel.MediaErrorType
|
||||
u2 uint16
|
||||
}{
|
||||
{
|
||||
p: 0,
|
||||
u1: 0,
|
||||
u2: 0,
|
||||
},
|
||||
{
|
||||
p: gtsmodel.NewMediaErrorDetails(gtsmodel.MediaErrorTypeNone, 0),
|
||||
u1: gtsmodel.MediaErrorTypeNone,
|
||||
u2: 0,
|
||||
},
|
||||
{
|
||||
p: gtsmodel.NewMediaErrorDetails(gtsmodel.MediaErrorTypeInterrupt, 0),
|
||||
u1: gtsmodel.MediaErrorTypeInterrupt,
|
||||
u2: 0,
|
||||
},
|
||||
{
|
||||
p: gtsmodel.NewMediaErrorDetails(gtsmodel.MediaErrorTypePolicy, gtsmodel.MediaErrorTypePolicy_Size),
|
||||
u1: gtsmodel.MediaErrorTypePolicy,
|
||||
u2: gtsmodel.MediaErrorTypePolicy_Size,
|
||||
},
|
||||
{
|
||||
p: gtsmodel.NewMediaErrorDetails(gtsmodel.MediaErrorTypePolicy, gtsmodel.MediaErrorTypePolicy_Domain),
|
||||
u1: gtsmodel.MediaErrorTypePolicy,
|
||||
u2: gtsmodel.MediaErrorTypePolicy_Domain,
|
||||
},
|
||||
{
|
||||
p: gtsmodel.NewMediaErrorDetails(gtsmodel.MediaErrorTypeNetwork, gtsmodel.MediaErrorTypeNetwork_DNS),
|
||||
u1: gtsmodel.MediaErrorTypeNetwork,
|
||||
u2: gtsmodel.MediaErrorTypeNetwork_DNS,
|
||||
},
|
||||
{
|
||||
p: gtsmodel.NewMediaErrorDetails(gtsmodel.MediaErrorTypeNetwork, gtsmodel.MediaErrorTypeNetwork_Timeout),
|
||||
u1: gtsmodel.MediaErrorTypeNetwork,
|
||||
u2: gtsmodel.MediaErrorTypeNetwork_Timeout,
|
||||
},
|
||||
{
|
||||
p: gtsmodel.NewMediaErrorDetails(gtsmodel.MediaErrorTypeHTTP, 400),
|
||||
u1: gtsmodel.MediaErrorTypeHTTP,
|
||||
u2: 400,
|
||||
},
|
||||
{
|
||||
p: gtsmodel.NewMediaErrorDetails(gtsmodel.MediaErrorTypeHTTP, 404),
|
||||
u1: gtsmodel.MediaErrorTypeHTTP,
|
||||
u2: 404,
|
||||
},
|
||||
{
|
||||
p: gtsmodel.NewMediaErrorDetails(gtsmodel.MediaErrorTypeHTTP, 500),
|
||||
u1: gtsmodel.MediaErrorTypeHTTP,
|
||||
u2: 500,
|
||||
},
|
||||
{
|
||||
p: gtsmodel.NewMediaErrorDetails(gtsmodel.MediaErrorTypeCodec, gtsmodel.MediaErrorTypeCodec_Unsupported),
|
||||
u1: gtsmodel.MediaErrorTypeCodec,
|
||||
u2: gtsmodel.MediaErrorTypeCodec_Unsupported,
|
||||
},
|
||||
}
|
||||
|
||||
func TestMediaErrorDetailsPack(t *testing.T) {
|
||||
for _, test := range mediaErrorDetailsTests {
|
||||
d := gtsmodel.NewMediaErrorDetails(test.u1, test.u2)
|
||||
u1, u2 := unpacku16s(uint32(d))
|
||||
assert.Equal(t, u1, uint16(test.u1))
|
||||
assert.Equal(t, u2, uint16(test.u2))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaErrorDetailsUnpack(t *testing.T) {
|
||||
for _, test := range mediaErrorDetailsTests {
|
||||
assert.Equal(t, test.u1, test.p.Type())
|
||||
assert.Equal(t, test.u2, test.p.Details())
|
||||
}
|
||||
}
|
||||
|
||||
func unpacku16s(u uint32) (u1, u2 uint16) {
|
||||
const bits = 16
|
||||
const mask = (1 << bits) - 1
|
||||
u1 = uint16(u >> bits)
|
||||
u2 = uint16(u & mask)
|
||||
return
|
||||
}
|
||||
@@ -102,3 +102,13 @@ func TimeFromULID(id string) (time.Time, error) {
|
||||
}
|
||||
return ulid.Time(parsed.Time()), nil
|
||||
}
|
||||
|
||||
// ZeroULIDForTime returns the zero-value ULID for given time.
|
||||
func ZeroULIDForTime(t time.Time) string {
|
||||
ts := ulid.Timestamp(t)
|
||||
var ulid ulid.ULID
|
||||
if err := ulid.SetTime(ts); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return ulid.String()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
// 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 media
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
|
||||
"codeberg.org/gruf/go-errors/v2"
|
||||
)
|
||||
|
||||
const codecDetails = gtsmodel.MediaErrorDetails(gtsmodel.MediaErrorTypeCodec)
|
||||
|
||||
// errWithDetails allows optionally wrapping an error,
|
||||
// but largely propagating MediaErrorDetails via error return.
|
||||
type errWithDetails struct {
|
||||
error
|
||||
details gtsmodel.MediaErrorDetails
|
||||
}
|
||||
|
||||
// withDetails wraps an optional error with given MediaErrorDetails as error type.
|
||||
func withDetails(err error, details gtsmodel.MediaErrorDetails) error {
|
||||
return &errWithDetails{err, details}
|
||||
}
|
||||
|
||||
func (err *errWithDetails) Error() string {
|
||||
if err.error == nil {
|
||||
// if no error was set, instead
|
||||
// use stringified details given.
|
||||
return err.details.String()
|
||||
}
|
||||
return err.error.Error()
|
||||
}
|
||||
|
||||
func (err *errWithDetails) Unwrap() error {
|
||||
return err.error
|
||||
}
|
||||
|
||||
// isStubError returns whether determined gtsmodel.MediaErrorDetails
|
||||
// was due to a "stubbing" type error, i.e. a sort of non-error.
|
||||
func isStubError(details gtsmodel.MediaErrorDetails) bool {
|
||||
return details.Type() == gtsmodel.MediaErrorTypePolicy ||
|
||||
details.Details() == gtsmodel.MediaErrorTypeCodec_Unsupported
|
||||
}
|
||||
|
||||
// toErrorDetails will convert given error to extracted MediaErrorDetails (if any).
|
||||
func toErrorDetails(err error) gtsmodel.MediaErrorDetails {
|
||||
if err == nil {
|
||||
// No error was returned, no details.
|
||||
return gtsmodel.NewMediaErrorDetails(
|
||||
gtsmodel.MediaErrorTypeNone,
|
||||
0,
|
||||
)
|
||||
|
||||
} else if withDetails := errors.AsV2[*errWithDetails](err); withDetails != nil {
|
||||
// Return stored err details.
|
||||
return withDetails.details
|
||||
|
||||
} else if errors.IsV2(err, context.Canceled, context.DeadlineExceeded) {
|
||||
// Interrupt error due to context cancelled.
|
||||
return gtsmodel.NewMediaErrorDetails(
|
||||
gtsmodel.MediaErrorTypeInterrupt,
|
||||
0,
|
||||
)
|
||||
|
||||
} else if details := extractNetworkErrorDetails(err); details != 0 {
|
||||
// Return determined
|
||||
// error details.
|
||||
return details
|
||||
}
|
||||
|
||||
// Any other type was unclassified error.
|
||||
return gtsmodel.NewMediaErrorDetails(
|
||||
gtsmodel.MediaErrorTypeUnknown,
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
// extractNetworkErrorDetails looks for and returns any network / http related details in error.
|
||||
func extractNetworkErrorDetails(err error) gtsmodel.MediaErrorDetails {
|
||||
if code := gtserror.StatusCode(err); code > 0 {
|
||||
// An HTTP status code was set, indicating error
|
||||
// due to HTTP response, extract and set details.
|
||||
return gtsmodel.NewMediaErrorDetails(
|
||||
gtsmodel.MediaErrorTypeHTTP,
|
||||
uint16(code), // nolint:gosec
|
||||
)
|
||||
|
||||
} else if netErr := errors.AsV2[interface{ Timeout() bool }](err); netErr != nil {
|
||||
var details uint16
|
||||
|
||||
// All "net{,/http}" package errors implement
|
||||
// Timeout(), use this to set type and details.
|
||||
if netErr.Timeout() {
|
||||
details = gtsmodel.MediaErrorTypeNetwork_Timeout
|
||||
} else if _, isDNS := netErr.(*net.DNSError); isDNS {
|
||||
details = gtsmodel.MediaErrorTypeNetwork_DNS
|
||||
}
|
||||
|
||||
return gtsmodel.NewMediaErrorDetails(
|
||||
gtsmodel.MediaErrorTypeNetwork,
|
||||
details,
|
||||
)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -69,7 +69,6 @@ func ffmpegClearMetadata(ctx context.Context, outpath, inpath string) error {
|
||||
|
||||
// ffmpegGenerateWebpThumb generates a thumbnail webp from input media of any type, useful for any media.
|
||||
func ffmpegGenerateWebpThumb(ctx context.Context, inpath, outpath string, width, height int, pixfmt string) error {
|
||||
// Generate thumb with ffmpeg.
|
||||
return ffmpeg(ctx, inpath, outpath,
|
||||
|
||||
// Only log errors.
|
||||
@@ -181,7 +180,8 @@ func ffmpeg(ctx context.Context, inpath string, outpath string, args ...string)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error running: %w", err)
|
||||
} else if rc != 0 {
|
||||
return gtserror.Newf("non-zero return code %d (%s)", rc, stderr.B)
|
||||
err := gtserror.Newf("non-zero return code %d (%s)", rc, stderr.B)
|
||||
return withDetails(err, codecDetails)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -256,7 +256,7 @@ func ffprobe(ctx context.Context, filepath string) (*result, error) {
|
||||
// Convert raw result data.
|
||||
res, err := result.Process()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, withDetails(err, codecDetails)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
@@ -497,8 +497,17 @@ func (res *result) PixFmt() string {
|
||||
|
||||
// Process converts raw ffprobe result data into our more usable result{} type.
|
||||
func (res *ffprobeResult) Process() (*result, error) {
|
||||
|
||||
if res.Error != nil {
|
||||
return nil, res.Error
|
||||
// Only return ffprobe error if it's NOT
|
||||
// unsupported, otherwise we want other
|
||||
// code paths to handle this file case.
|
||||
if !res.Error.IsUnsupported() {
|
||||
return nil, res.Error
|
||||
}
|
||||
|
||||
// Return loggable format string indicating the issue.
|
||||
return &result{format: "unsupported by ffprobe"}, nil
|
||||
}
|
||||
|
||||
if res.Format == nil {
|
||||
@@ -698,14 +707,17 @@ type ffprobeFormat struct {
|
||||
BitRate string `json:"bit_rate"`
|
||||
}
|
||||
|
||||
// ffprobeError is a representation of the JSON
|
||||
// error details that ffprobe may return on attempted
|
||||
// probing of media file for details. it has helper
|
||||
// methods to allow it to implement error{}
|
||||
type ffprobeError struct {
|
||||
Code int `json:"code"`
|
||||
String string `json:"string"`
|
||||
}
|
||||
|
||||
func isUnsupportedTypeErr(err error) bool {
|
||||
ffprobeErr, ok := err.(*ffprobeError)
|
||||
return ok && ffprobeErr.Code == -1094995529
|
||||
func (err *ffprobeError) IsUnsupported() bool {
|
||||
return err.Code == -1094995529
|
||||
}
|
||||
|
||||
func (err *ffprobeError) Error() string {
|
||||
|
||||
+51
-14
@@ -112,14 +112,12 @@ func (m *Manager) CreateMedia(
|
||||
// leaving out fields with values we don't know
|
||||
// yet. These will be overwritten as we go.
|
||||
attachment := >smodel.MediaAttachment{
|
||||
ID: id.NewULID(),
|
||||
AccountID: accountID,
|
||||
Type: gtsmodel.FileTypeUnknown,
|
||||
Processing: gtsmodel.ProcessingStatusReceived,
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(false),
|
||||
CreatedAt: now,
|
||||
ID: id.NewULID(),
|
||||
AccountID: accountID,
|
||||
Type: gtsmodel.FileTypeUnknown,
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
CreatedAt: now,
|
||||
}
|
||||
|
||||
// Check if we were provided additional info
|
||||
@@ -172,11 +170,24 @@ func (m *Manager) CacheMedia(
|
||||
data DataFunc,
|
||||
info AdditionalMediaInfo,
|
||||
) *ProcessingMedia {
|
||||
var stubErr error
|
||||
|
||||
if reason := info.RejectReason; reason != nil {
|
||||
// If a predefined reject reason was provided,
|
||||
// don't download and return early with error.
|
||||
stubErr = &errWithDetails{details: *reason}
|
||||
} else if details := media.Error; !details.SupportsRetry() {
|
||||
// If failed to download due to an existing error,
|
||||
// don't attempt redownload, return early with error.
|
||||
err := gtserror.New("unretryable error: " + details.String())
|
||||
stubErr = &errWithDetails{error: err, details: details}
|
||||
}
|
||||
|
||||
return &ProcessingMedia{
|
||||
media: media,
|
||||
dataFn: data,
|
||||
mgr: m,
|
||||
stubOnly: util.PtrOrZero(info.RejectMedia),
|
||||
media: media,
|
||||
dataFn: data,
|
||||
mgr: m,
|
||||
err: stubErr,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,13 +355,26 @@ func (m *Manager) CacheEmoji(
|
||||
pathID = id
|
||||
}
|
||||
|
||||
var stubErr error
|
||||
|
||||
if reason := info.RejectReason; reason != nil {
|
||||
// If a predefined reject reason was provided,
|
||||
// don't download and return early with error.
|
||||
stubErr = &errWithDetails{details: *reason}
|
||||
} else if details := emoji.Error; !details.SupportsRetry() {
|
||||
// If failed to download due to an existing error,
|
||||
// don't attempt redownload, return early with error.
|
||||
err := gtserror.New("unretryable error: " + details.String())
|
||||
stubErr = &errWithDetails{error: err, details: details}
|
||||
}
|
||||
|
||||
return &ProcessingEmoji{
|
||||
newPathID: pathID,
|
||||
instAccID: instanceAcc.ID,
|
||||
emoji: emoji,
|
||||
dataFn: data,
|
||||
mgr: m,
|
||||
stubOnly: util.PtrOrZero(info.RejectMedia),
|
||||
err: stubErr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -374,6 +398,8 @@ func (m *Manager) createOrUpdateEmoji(
|
||||
return nil, gtserror.Newf("error fetching instance account: %w", err)
|
||||
}
|
||||
|
||||
var stubErr error
|
||||
|
||||
// Check if we have additional info to add to the emoji,
|
||||
// and overwrite some of the emoji fields if so.
|
||||
if info.URI != nil {
|
||||
@@ -397,6 +423,16 @@ func (m *Manager) createOrUpdateEmoji(
|
||||
if info.CategoryID != nil {
|
||||
emoji.CategoryID = *info.CategoryID
|
||||
}
|
||||
if reason := info.RejectReason; reason != nil {
|
||||
// If a predefined reject reason was provided,
|
||||
// don't download and return early with error.
|
||||
stubErr = &errWithDetails{details: *reason}
|
||||
} else if details := emoji.Error; !details.SupportsRetry() {
|
||||
// If failed to download due to an existing error,
|
||||
// don't attempt redownload, return early with error.
|
||||
err := gtserror.New("unretryable error: " + details.String())
|
||||
stubErr = &errWithDetails{error: err, details: details}
|
||||
}
|
||||
|
||||
// Put or update emoji in database.
|
||||
if err := storeDB(ctx, emoji); err != nil {
|
||||
@@ -409,7 +445,7 @@ func (m *Manager) createOrUpdateEmoji(
|
||||
emoji: emoji,
|
||||
dataFn: data,
|
||||
mgr: m,
|
||||
stubOnly: util.PtrOrZero(info.RejectMedia),
|
||||
err: stubErr,
|
||||
}
|
||||
|
||||
return processingEmoji, nil
|
||||
@@ -417,6 +453,7 @@ func (m *Manager) createOrUpdateEmoji(
|
||||
|
||||
// extractEmojiPathID pulls the ID used in the final path segment of an emoji path (can be URL).
|
||||
func extractEmojiPathID(path string) string {
|
||||
|
||||
// Look for '.' indicating file ext.
|
||||
i := strings.LastIndexByte(path, '.')
|
||||
if i == -1 {
|
||||
|
||||
@@ -86,7 +86,8 @@ func (suite *ManagerTestSuite) TestEmojiProcess() {
|
||||
func (suite *ManagerTestSuite) TestEmojiProcessRefresh() {
|
||||
ctx := suite.T().Context()
|
||||
|
||||
// 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"]
|
||||
|
||||
emojiToUpdate, err := suite.db.GetEmojiByID(ctx, originalEmoji.ID)
|
||||
@@ -186,8 +187,19 @@ func (suite *ManagerTestSuite) TestEmojiProcessTooLarge() {
|
||||
suite.NoError(err)
|
||||
|
||||
// do a blocking call to fetch the emoji
|
||||
_, err = processing.Load(ctx)
|
||||
suite.EqualError(err, "store: error draining data to tmp: reached read limit 630kiB")
|
||||
emoji, err := processing.Load(ctx)
|
||||
suite.NoError(err)
|
||||
|
||||
// now make sure the emoji is in the database
|
||||
dbEmoji, err := suite.db.GetEmojiByID(ctx, emoji.ID)
|
||||
suite.NoError(err)
|
||||
suite.NotNil(dbEmoji)
|
||||
|
||||
// Emoji should have an appropriate error.
|
||||
suite.Equal(gtsmodel.NewMediaErrorDetails(
|
||||
gtsmodel.MediaErrorTypePolicy,
|
||||
gtsmodel.MediaErrorTypePolicy_Size,
|
||||
), emoji.Error)
|
||||
}
|
||||
|
||||
func (suite *ManagerTestSuite) TestEmojiWebpProcess() {
|
||||
@@ -320,8 +332,20 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessTooLarge() {
|
||||
suite.NotNil(processing)
|
||||
|
||||
// do a blocking call to fetch the attachment
|
||||
_, err = processing.Load(ctx)
|
||||
suite.EqualError(err, "store: error draining data to tmp: reached read limit 263kiB")
|
||||
attachment, err := processing.Load(ctx)
|
||||
suite.NoError(err)
|
||||
|
||||
// now make sure the attachment is in the database
|
||||
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
|
||||
suite.NoError(err)
|
||||
suite.NotNil(dbAttachment)
|
||||
|
||||
// Attachment should have type unknown and appropriate error.
|
||||
suite.Equal(gtsmodel.FileTypeUnknown, dbAttachment.Type)
|
||||
suite.Equal(gtsmodel.NewMediaErrorDetails(
|
||||
gtsmodel.MediaErrorTypePolicy,
|
||||
gtsmodel.MediaErrorTypePolicy_Size,
|
||||
), dbAttachment.Error)
|
||||
}
|
||||
|
||||
func (suite *ManagerTestSuite) TestPDFProcess() {
|
||||
@@ -368,16 +392,16 @@ func (suite *ManagerTestSuite) TestPDFProcess() {
|
||||
suite.NoError(err)
|
||||
suite.NotNil(dbAttachment)
|
||||
|
||||
// Attachment should have type unknown
|
||||
// Attachment should have type unknown and appropriate error.
|
||||
suite.Equal(gtsmodel.FileTypeUnknown, dbAttachment.Type)
|
||||
suite.Equal(gtsmodel.NewMediaErrorDetails(
|
||||
gtsmodel.MediaErrorTypeCodec,
|
||||
gtsmodel.MediaErrorTypeCodec_Unsupported,
|
||||
), dbAttachment.Error)
|
||||
|
||||
// Nothing should be in storage for this attachment.
|
||||
stored, err := suite.storage.Has(ctx, attachment.File.Path)
|
||||
suite.NoError(err)
|
||||
suite.False(stored)
|
||||
stored, err = suite.storage.Has(ctx, attachment.Thumbnail.Path)
|
||||
suite.NoError(err)
|
||||
suite.False(stored)
|
||||
// Nothing should be in storage for attachment.
|
||||
suite.Empty(dbAttachment.Thumbnail.Path)
|
||||
suite.Empty(dbAttachment.File.Path)
|
||||
}
|
||||
|
||||
func (suite *ManagerTestSuite) TestSlothVineProcess() {
|
||||
@@ -1075,7 +1099,10 @@ func (suite *ManagerTestSuite) TestUncacheRejectedMedia() {
|
||||
attachment,
|
||||
nil,
|
||||
media.AdditionalMediaInfo{
|
||||
RejectMedia: util.Ptr(true),
|
||||
RejectReason: util.Ptr(gtsmodel.NewMediaErrorDetails(
|
||||
gtsmodel.MediaErrorTypePolicy,
|
||||
gtsmodel.MediaErrorTypePolicy_Domain,
|
||||
)),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1086,11 +1113,16 @@ func (suite *ManagerTestSuite) TestUncacheRejectedMedia() {
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
suite.False(*attachment.Cached)
|
||||
suite.False(attachment.Cached())
|
||||
suite.Equal(gtsmodel.FileTypeUnknown, attachment.Type)
|
||||
suite.Zero(attachment.File)
|
||||
suite.Zero(attachment.FileMeta)
|
||||
suite.Zero(attachment.Thumbnail)
|
||||
suite.Empty(attachment.Thumbnail.Path)
|
||||
suite.Empty(attachment.File.Path)
|
||||
|
||||
// The reject reason error should be stored.
|
||||
suite.Equal(gtsmodel.NewMediaErrorDetails(
|
||||
gtsmodel.MediaErrorTypePolicy,
|
||||
gtsmodel.MediaErrorTypePolicy_Domain,
|
||||
), attachment.Error)
|
||||
|
||||
// Blurhash + description
|
||||
// should be preserved.
|
||||
|
||||
@@ -100,7 +100,8 @@ func terminateExif(outpath, inpath string, ext string) (err error) {
|
||||
// Terminate EXIF data from 'inFile' -> 'outFile'.
|
||||
err = terminator.TerminateInto(outFile, inFile, ext)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error terminating exif data: %w", err)
|
||||
err := gtserror.Newf("error terminating exif data: %w", err)
|
||||
return withDetails(err, codecDetails)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -87,7 +87,8 @@ func probeJPEG(file fileReader) (*result, error) {
|
||||
file,
|
||||
))
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error decoding file %s: %w", file.Name(), err)
|
||||
err := gtserror.Newf("error decoding file %s: %w", file.Name(), err)
|
||||
return nil, withDetails(err, codecDetails)
|
||||
}
|
||||
|
||||
// Jump back to file start.
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/storage"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/uris"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/util"
|
||||
"codeberg.org/gruf/go-errors/v2"
|
||||
errorsv2 "codeberg.org/gruf/go-errors/v2"
|
||||
"codeberg.org/gruf/go-runners"
|
||||
)
|
||||
@@ -34,41 +34,38 @@ import (
|
||||
// ProcessingEmoji represents an emoji currently processing. It exposes
|
||||
// various functions for retrieving data from the process.
|
||||
type ProcessingEmoji struct {
|
||||
// Processing emoji details.
|
||||
|
||||
// processing emoji details.
|
||||
emoji *gtsmodel.Emoji
|
||||
|
||||
// Instance account ID, used to
|
||||
// instance account ID, used to
|
||||
// construct emoji storage path.
|
||||
instAccID string
|
||||
|
||||
// New emoji path ID to
|
||||
// new emoji path ID to
|
||||
// use when being refreshed.
|
||||
newPathID string
|
||||
|
||||
// Load-data function,
|
||||
// load data function,
|
||||
// returns media stream.
|
||||
dataFn DataFunc
|
||||
|
||||
// done is set when process finishes
|
||||
// with non ctx canceled type error
|
||||
done bool
|
||||
|
||||
// proc helps synchronize only a
|
||||
// singular running processing instance
|
||||
proc runners.Processor
|
||||
|
||||
// error stores permanent
|
||||
// error value when done
|
||||
// error stores permanent value when done,
|
||||
// or alternatively may store a preset stubError{}
|
||||
// value with details to allow skipping processing.
|
||||
err error
|
||||
|
||||
// mgr instance, for access to
|
||||
// db / storage during processing
|
||||
mgr *Manager
|
||||
|
||||
// true if this emoji should not
|
||||
// be downloaded, ie., should be
|
||||
// returned as placeholder only.
|
||||
stubOnly bool
|
||||
// done is set when process finishes
|
||||
// with non ctx canceled type error
|
||||
done bool
|
||||
}
|
||||
|
||||
// Load blocks until the static and fullsize image has been processed, and then returns the completed emoji.
|
||||
@@ -101,7 +98,6 @@ func (p *ProcessingEmoji) Placeholder() *gtsmodel.Emoji {
|
||||
emoji.ID = p.emoji.ID
|
||||
emoji.Shortcode = p.emoji.Shortcode
|
||||
emoji.Domain = p.emoji.Domain
|
||||
emoji.Cached = new(bool)
|
||||
emoji.ImageRemoteURL = p.emoji.ImageRemoteURL
|
||||
emoji.ImageStaticRemoteURL = p.emoji.ImageStaticRemoteURL
|
||||
emoji.Disabled = p.emoji.Disabled
|
||||
@@ -133,10 +129,11 @@ func (p *ProcessingEmoji) load(ctx context.Context) (
|
||||
done bool,
|
||||
err error,
|
||||
) {
|
||||
err = p.proc.Process(func() error {
|
||||
err = p.proc.Process(func() (err error) {
|
||||
if done = p.done; done {
|
||||
// Already proc'd.
|
||||
return p.err
|
||||
err = p.err
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
@@ -153,33 +150,36 @@ func (p *ProcessingEmoji) load(ctx context.Context) (
|
||||
// (i.e. no ctx canceled).
|
||||
ctx = context.WithoutCancel(ctx)
|
||||
|
||||
// Store values.
|
||||
p.done = done
|
||||
p.err = err
|
||||
|
||||
// On error or stub, ensure
|
||||
// no downloaded files remain.
|
||||
if err != nil || p.stubOnly {
|
||||
if err != nil {
|
||||
p.cleanup(ctx)
|
||||
}
|
||||
|
||||
// Check the extracted error details on emoji for
|
||||
// stub type error. i.e. policy or media type issue.
|
||||
if isStubError(p.emoji.Error) {
|
||||
log.Warnf(ctx, "stubbed %s due to: %v", p.emoji.ImageRemoteURL, p.err)
|
||||
err = nil // don't return stub errors
|
||||
}
|
||||
|
||||
// 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 values.
|
||||
p.done = true
|
||||
p.err = err
|
||||
}
|
||||
}()
|
||||
|
||||
// If we're only stubbing, skip
|
||||
// calling store() to cache the emoji.
|
||||
//
|
||||
// Any files that may have been stored
|
||||
// for it previously will be cleaned up
|
||||
// by the deferred function above.
|
||||
if p.stubOnly {
|
||||
err = nil
|
||||
return err
|
||||
// If existing error details exists, check if supports retry.
|
||||
if withDetails := errors.AsV2[*errWithDetails](p.err); //
|
||||
withDetails != nil && !withDetails.details.SupportsRetry() {
|
||||
err = p.err
|
||||
return
|
||||
}
|
||||
|
||||
// Attempt to store media and calculate
|
||||
@@ -187,7 +187,7 @@ func (p *ProcessingEmoji) load(ctx context.Context) (
|
||||
//
|
||||
// This will update p.emoji as it goes.
|
||||
err = p.store(ctx)
|
||||
return err
|
||||
return
|
||||
})
|
||||
|
||||
// Return a copy of emoji.
|
||||
@@ -200,9 +200,17 @@ func (p *ProcessingEmoji) load(ctx context.Context) (
|
||||
// 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.
|
||||
func (p *ProcessingEmoji) store(ctx context.Context) error {
|
||||
|
||||
// Load media from data func.
|
||||
rc, err := p.dataFn(ctx)
|
||||
if err != nil {
|
||||
|
||||
// If a network error, include these details.
|
||||
if details := extractNetworkErrorDetails(err); //
|
||||
details != 0 {
|
||||
err = withDetails(err, details)
|
||||
}
|
||||
|
||||
return gtserror.Newf("error executing data function: %w", err)
|
||||
}
|
||||
|
||||
@@ -230,11 +238,8 @@ func (p *ProcessingEmoji) store(ctx context.Context) error {
|
||||
// Pass input file through ffprobe to
|
||||
// parse further metadata information.
|
||||
result, err := probe(ctx, temppath)
|
||||
if err != nil && !isUnsupportedTypeErr(err) {
|
||||
if err != nil {
|
||||
return gtserror.Newf("ffprobe error: %w", err)
|
||||
} else if result == nil {
|
||||
log.Warnf(ctx, "unsupported data type by ffprobe: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
var ext string
|
||||
@@ -243,7 +248,10 @@ func (p *ProcessingEmoji) store(ctx context.Context) error {
|
||||
// Get abstract file type, mimetype and ext from ffprobe data.
|
||||
fileType, p.emoji.ImageContentType, ext = result.GetFileType()
|
||||
if fileType != gtsmodel.FileTypeImage {
|
||||
return gtserror.Newf("unsupported emoji filetype: %s (%s)", fileType, ext)
|
||||
|
||||
// NOTE: unlike regular media, unsupported file type is an error for emoji.
|
||||
err := gtserror.Newf("unsupported emoji filetype: %s (%s)", fileType, ext)
|
||||
return withDetails(err, codecDetails)
|
||||
}
|
||||
|
||||
// Add file extension to path.
|
||||
@@ -337,14 +345,17 @@ func (p *ProcessingEmoji) store(ctx context.Context) error {
|
||||
"png",
|
||||
)
|
||||
|
||||
// We can now consider this cached.
|
||||
p.emoji.Cached = util.Ptr(true)
|
||||
// Success! Unset previous
|
||||
// error details for emoji.
|
||||
p.emoji.Error = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleanup will remove any traces of processing emoji from storage,
|
||||
// and perform any other necessary cleanup steps after failure.
|
||||
//
|
||||
// details of any error are extracted and can be accessed via p.emoji.Error.
|
||||
func (p *ProcessingEmoji) cleanup(ctx context.Context) {
|
||||
log.Debugf(ctx, "running cleanup of emoji %s", p.emoji.ID)
|
||||
|
||||
@@ -364,16 +375,9 @@ func (p *ProcessingEmoji) cleanup(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Unset processor-calculated fields.
|
||||
p.emoji.ImageStaticContentType = ""
|
||||
p.emoji.ImageStaticFileSize = 0
|
||||
p.emoji.ImageStaticPath = ""
|
||||
p.emoji.ImageStaticURL = ""
|
||||
p.emoji.ImageContentType = ""
|
||||
p.emoji.ImageFileSize = 0
|
||||
p.emoji.ImagePath = ""
|
||||
p.emoji.ImageURL = ""
|
||||
// Unset fields.
|
||||
p.emoji.Stub()
|
||||
|
||||
// Ensure marked as not cached.
|
||||
p.emoji.Cached = util.Ptr(false)
|
||||
// Extract any error details for db.
|
||||
p.emoji.Error = toErrorDetails(p.err)
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"codeberg.org/gruf/go-errors/v2"
|
||||
errorsv2 "codeberg.org/gruf/go-errors/v2"
|
||||
"codeberg.org/gruf/go-kv/v2"
|
||||
"codeberg.org/gruf/go-runners"
|
||||
|
||||
"code.superseriousbusiness.org/gopkg/log"
|
||||
@@ -38,34 +38,30 @@ import (
|
||||
// currently being processed. It exposes functions
|
||||
// for retrieving data from the process.
|
||||
type ProcessingMedia struct {
|
||||
// Processing media
|
||||
// attachment details.
|
||||
|
||||
// processing media attach details.
|
||||
media *gtsmodel.MediaAttachment
|
||||
|
||||
// Load-data function,
|
||||
// load data function,
|
||||
// returns media stream.
|
||||
dataFn DataFunc
|
||||
|
||||
// done is set when process finishes
|
||||
// with non ctx canceled type error
|
||||
done bool
|
||||
|
||||
// proc helps synchronize only a
|
||||
// singular running processing instance
|
||||
proc runners.Processor
|
||||
|
||||
// error stores permanent
|
||||
// error value when done
|
||||
// error stores permanent value when done,
|
||||
// or alternatively may store a preset stubError{}
|
||||
// value with details to allow skipping processing.
|
||||
err error
|
||||
|
||||
// mgr instance, for access to
|
||||
// db / storage during processing
|
||||
mgr *Manager
|
||||
|
||||
// true if this piece of media should
|
||||
// not be downloaded, ie., should be
|
||||
// stubbed as an Unknown type only
|
||||
stubOnly bool
|
||||
// done is set when process finishes
|
||||
// with non ctx canceled type error
|
||||
done bool
|
||||
}
|
||||
|
||||
// MustLoad blocks until the thumbnail and fullsize image has been processed, and then returns the completed media.
|
||||
@@ -100,10 +96,8 @@ func (p *ProcessingMedia) Placeholder() *gtsmodel.MediaAttachment {
|
||||
media.StatusID = p.media.StatusID
|
||||
media.ScheduledStatusID = p.media.ScheduledStatusID
|
||||
media.Description = p.media.Description
|
||||
media.Processing = p.media.Processing
|
||||
media.Avatar = p.media.Avatar
|
||||
media.Header = p.media.Header
|
||||
media.Cached = new(bool)
|
||||
media.RemoteURL = p.media.RemoteURL
|
||||
media.Thumbnail.RemoteURL = p.media.Thumbnail.RemoteURL
|
||||
media.Blurhash = p.media.Blurhash
|
||||
@@ -127,10 +121,11 @@ func (p *ProcessingMedia) load(ctx context.Context) (
|
||||
done bool,
|
||||
err error,
|
||||
) {
|
||||
err = p.proc.Process(func() error {
|
||||
err = p.proc.Process(func() (err error) {
|
||||
if done = p.done; done {
|
||||
// Already proc'd.
|
||||
return p.err
|
||||
err = p.err
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
@@ -147,33 +142,36 @@ func (p *ProcessingMedia) load(ctx context.Context) (
|
||||
// (i.e. no ctx canceled).
|
||||
ctx = context.WithoutCancel(ctx)
|
||||
|
||||
// On error, stub, or unknown media
|
||||
// types, perform error cleanup.
|
||||
if err != nil || p.stubOnly || p.media.Type == gtsmodel.FileTypeUnknown {
|
||||
// Store values.
|
||||
p.done = done
|
||||
p.err = err
|
||||
|
||||
// If any error value is stored, including
|
||||
// stubError (e.g. unknown type), do cleanup().
|
||||
if p.err != nil {
|
||||
p.cleanup(ctx)
|
||||
}
|
||||
|
||||
// Check the extracted error details on media for
|
||||
// stub type error. i.e. policy or media type issue.
|
||||
if isStubError(p.media.Error) {
|
||||
log.Warnf(ctx, "stubbed %s due to: %v", p.media.RemoteURL, p.err)
|
||||
err = nil // don't return stub errors
|
||||
}
|
||||
|
||||
// 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 values.
|
||||
p.done = true
|
||||
p.err = err
|
||||
}
|
||||
}()
|
||||
|
||||
// If we're only stubbing, skip
|
||||
// calling store() to cache the media.
|
||||
//
|
||||
// Any files that may have been stored
|
||||
// for it previously will be cleaned up
|
||||
// by the deferred function above.
|
||||
if p.stubOnly {
|
||||
err = nil
|
||||
return err
|
||||
// If existing error details exists, check if supports retry.
|
||||
if withDetails := errors.AsV2[*errWithDetails](p.err); //
|
||||
withDetails != nil && !withDetails.details.SupportsRetry() {
|
||||
err = p.err
|
||||
return
|
||||
}
|
||||
|
||||
// Attempt to store media and calculate
|
||||
@@ -181,7 +179,7 @@ func (p *ProcessingMedia) load(ctx context.Context) (
|
||||
//
|
||||
// This will update p.media as it goes.
|
||||
err = p.store(ctx)
|
||||
return err
|
||||
return
|
||||
})
|
||||
|
||||
// Return a copy of media attachment.
|
||||
@@ -194,9 +192,17 @@ func (p *ProcessingMedia) load(ctx context.Context) (
|
||||
// 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.
|
||||
func (p *ProcessingMedia) store(ctx context.Context) error {
|
||||
|
||||
// Load media from data func.
|
||||
rc, err := p.dataFn(ctx)
|
||||
if err != nil {
|
||||
|
||||
// If a network error, include these details.
|
||||
if details := extractNetworkErrorDetails(err); //
|
||||
details != 0 {
|
||||
err = withDetails(err, details)
|
||||
}
|
||||
|
||||
return gtserror.Newf("error executing data function: %w", err)
|
||||
}
|
||||
|
||||
@@ -224,11 +230,8 @@ func (p *ProcessingMedia) store(ctx context.Context) error {
|
||||
// Pass input file through ffprobe to
|
||||
// parse further metadata information.
|
||||
result, err := probe(ctx, temppath)
|
||||
if err != nil && !isUnsupportedTypeErr(err) {
|
||||
if err != nil {
|
||||
return gtserror.Newf("ffprobe error: %w", err)
|
||||
} else if result == nil {
|
||||
log.Warnf(ctx, "unsupported data type by ffprobe: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
var ext string
|
||||
@@ -248,6 +251,15 @@ func (p *ProcessingMedia) store(ctx context.Context) error {
|
||||
|
||||
// Set generic media type and mimetype from ffprobe format data.
|
||||
p.media.Type, p.media.File.ContentType, ext = result.GetFileType()
|
||||
if p.media.Type == gtsmodel.FileTypeUnknown {
|
||||
|
||||
// On unsupported return a stub error that doesn't
|
||||
// get returned to the caller, but indicates details.
|
||||
return withDetails(nil, gtsmodel.NewMediaErrorDetails(
|
||||
gtsmodel.MediaErrorTypeCodec,
|
||||
gtsmodel.MediaErrorTypeCodec_Unsupported,
|
||||
))
|
||||
}
|
||||
|
||||
// Add file extension to path.
|
||||
newpath := temppath + "." + ext
|
||||
@@ -273,13 +285,6 @@ func (p *ProcessingMedia) store(ctx context.Context) error {
|
||||
case gtsmodel.FileTypeAudio:
|
||||
// NOTE: we do not clean audio file
|
||||
// metadata, in order to keep tags.
|
||||
|
||||
default:
|
||||
log.WarnKVs(ctx, kv.Fields{
|
||||
{K: "format", V: result.format},
|
||||
{K: "msg", V: "unsupported data type"},
|
||||
}...)
|
||||
return nil
|
||||
}
|
||||
|
||||
if width > 0 && height > 0 {
|
||||
@@ -387,11 +392,9 @@ func (p *ProcessingMedia) store(ctx context.Context) error {
|
||||
ext,
|
||||
)
|
||||
|
||||
// We can now consider this cached.
|
||||
p.media.Cached = util.Ptr(true)
|
||||
|
||||
// Finally set the attachment as finished processing.
|
||||
p.media.Processing = gtsmodel.ProcessingStatusProcessed
|
||||
// Success! Unset previous
|
||||
// error details for media.
|
||||
p.media.Error = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -415,21 +418,13 @@ func (p *ProcessingMedia) cleanup(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Unset all processor-calculated media fields.
|
||||
p.media.FileMeta.Original = gtsmodel.Original{}
|
||||
p.media.FileMeta.Small = gtsmodel.Small{}
|
||||
p.media.File.ContentType = ""
|
||||
p.media.File.FileSize = 0
|
||||
p.media.File.Path = ""
|
||||
p.media.Thumbnail.FileSize = 0
|
||||
p.media.Thumbnail.ContentType = ""
|
||||
p.media.Thumbnail.Path = ""
|
||||
p.media.Thumbnail.URL = ""
|
||||
p.media.URL = ""
|
||||
// Unset fields.
|
||||
p.media.Stub()
|
||||
|
||||
// Also ensure marked as unknown and finished
|
||||
// processing so gets inserted as placeholder URL.
|
||||
p.media.Processing = gtsmodel.ProcessingStatusProcessed
|
||||
// Extract any error details for db.
|
||||
p.media.Error = toErrorDetails(p.err)
|
||||
|
||||
// Also ensure marked as unknown
|
||||
// so gets inserted as placeholder URL.
|
||||
p.media.Type = gtsmodel.FileTypeUnknown
|
||||
p.media.Cached = util.Ptr(false)
|
||||
}
|
||||
|
||||
@@ -242,7 +242,8 @@ func generateNativeThumb(
|
||||
_ = infile.Close()
|
||||
|
||||
if err != nil {
|
||||
return "", gtserror.Newf("error decoding file %s: %w", inpath, err)
|
||||
err := gtserror.Newf("error decoding file %s: %w", inpath, err)
|
||||
return "", withDetails(err, codecDetails)
|
||||
}
|
||||
|
||||
// Apply orientation BEFORE any resize,
|
||||
@@ -284,7 +285,8 @@ func generateNativeThumb(
|
||||
_ = outfile.Close()
|
||||
|
||||
if err != nil {
|
||||
return "", gtserror.Newf("error encoding image: %w", err)
|
||||
err := gtserror.Newf("error encoding image: %w", err)
|
||||
return "", withDetails(err, codecDetails)
|
||||
}
|
||||
|
||||
if needBlurhash {
|
||||
@@ -301,7 +303,8 @@ func generateNativeThumb(
|
||||
// Generate blurhash for the tiny thumbnail.
|
||||
blurhash, err := blurhash.Encode(4, 3, tiny)
|
||||
if err != nil {
|
||||
return "", gtserror.Newf("error generating blurhash: %w", err)
|
||||
err := gtserror.Newf("error generating blurhash: %w", err)
|
||||
return "", withDetails(err, codecDetails)
|
||||
}
|
||||
|
||||
return blurhash, nil
|
||||
@@ -326,7 +329,8 @@ func generateWebpBlurhash(filepath string) (string, error) {
|
||||
_ = file.Close()
|
||||
|
||||
if err != nil {
|
||||
return "", gtserror.Newf("error decoding file %s: %w", filepath, err)
|
||||
err := gtserror.Newf("error decoding file %s: %w", filepath, err)
|
||||
return "", withDetails(err, codecDetails)
|
||||
}
|
||||
|
||||
// for generating blurhashes, it's more
|
||||
@@ -342,7 +346,8 @@ func generateWebpBlurhash(filepath string) (string, error) {
|
||||
// Generate blurhash for the tiny thumbnail.
|
||||
blurhash, err := blurhash.Encode(4, 3, tiny)
|
||||
if err != nil {
|
||||
return "", gtserror.Newf("error generating blurhash: %w", err)
|
||||
err := gtserror.Newf("error generating blurhash: %w", err)
|
||||
return "", withDetails(err, codecDetails)
|
||||
}
|
||||
|
||||
return blurhash, nil
|
||||
|
||||
+8
-10
@@ -20,6 +20,8 @@ package media
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
type Size string
|
||||
@@ -79,11 +81,9 @@ type AdditionalMediaInfo struct {
|
||||
// this media; defaults to 0.
|
||||
FocusY *float32
|
||||
|
||||
// Set this to "true" if media originates
|
||||
// from a domain that has a domain limit
|
||||
// in place with a media policy of "reject".
|
||||
// In this case, the media will not downloaded.
|
||||
RejectMedia *bool
|
||||
// Set this if media should be rejected due to
|
||||
// some predetermined reason, e.g. domain policy.
|
||||
RejectReason *gtsmodel.MediaErrorDetails
|
||||
}
|
||||
|
||||
// AdditionalEmojiInfo represents additional information
|
||||
@@ -118,11 +118,9 @@ type AdditionalEmojiInfo struct {
|
||||
// should be placed in; defaults to "".
|
||||
CategoryID *string
|
||||
|
||||
// Set this to "true" if emoji originates
|
||||
// from a domain that has a domain limit
|
||||
// in place with a media policy of "reject".
|
||||
// In this case, the emoji will not downloaded.
|
||||
RejectMedia *bool
|
||||
// Set this if media should be rejected due to
|
||||
// some predetermined reason, e.g. domain policy.
|
||||
RejectReason *gtsmodel.MediaErrorDetails
|
||||
}
|
||||
|
||||
// DataFunc represents a function used to retrieve the raw bytes of a piece of media.
|
||||
|
||||
@@ -27,8 +27,7 @@ import (
|
||||
"runtime"
|
||||
"syscall"
|
||||
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
|
||||
"codeberg.org/gruf/go-bytesize"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
|
||||
"codeberg.org/gruf/go-iotools"
|
||||
"codeberg.org/gruf/go-mmap"
|
||||
)
|
||||
@@ -159,7 +158,6 @@ func drainToTmp(rc io.ReadCloser) (string, error) {
|
||||
|
||||
// Limited reader (if any).
|
||||
var lr *io.LimitedReader
|
||||
var limit int64
|
||||
|
||||
// Reader type to use
|
||||
// for draining to tmp.
|
||||
@@ -174,7 +172,7 @@ func drainToTmp(rc io.ReadCloser) (string, error) {
|
||||
rd = rct.Reader
|
||||
|
||||
// Extract limited reader if wrapped.
|
||||
lr, limit = iotools.GetReaderLimit(rd)
|
||||
lr, _ = iotools.GetReaderLimit(rd)
|
||||
}
|
||||
|
||||
// Drain reader into tmp.
|
||||
@@ -186,8 +184,10 @@ func drainToTmp(rc io.ReadCloser) (string, error) {
|
||||
// Check to see if limit was reached,
|
||||
// (produces more useful error messages).
|
||||
if lr != nil && lr.N <= 0 {
|
||||
err := fmt.Errorf("reached read limit %s", bytesize.Size(limit)) // #nosec G115 -- Just logging
|
||||
return path, gtserror.SetLimitReached(err)
|
||||
return path, withDetails(nil, gtsmodel.NewMediaErrorDetails(
|
||||
gtsmodel.MediaErrorTypePolicy,
|
||||
gtsmodel.MediaErrorTypePolicy_Size,
|
||||
))
|
||||
}
|
||||
|
||||
return path, nil
|
||||
|
||||
@@ -35,7 +35,6 @@ import (
|
||||
"code.superseriousbusiness.org/gotosocial/internal/regexes"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/storage"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/uris"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// GetFile retrieves a file from storage and streams it back
|
||||
@@ -95,8 +94,7 @@ func (p *Processor) GetFile(
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there's a limit on
|
||||
// the account's (sub)domain.
|
||||
// Check if there's a limit on the account's (sub)domain.
|
||||
limit, err := p.state.DB.MatchDomainLimit(ctx, acct.Domain)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error matching domain limit: %w", err)
|
||||
@@ -184,20 +182,24 @@ func (p *Processor) getAttachmentContent(
|
||||
// Start preparing API content model and other
|
||||
// values depending on requested media size.
|
||||
var content apimodel.Content
|
||||
var mediaPath string
|
||||
var mediaPath func(*gtsmodel.MediaAttachment) string
|
||||
switch sizeStr {
|
||||
|
||||
// Original media size.
|
||||
case media.SizeOriginal:
|
||||
content.ContentType = attach.File.ContentType
|
||||
content.ContentLength = int64(attach.File.FileSize)
|
||||
mediaPath = attach.File.Path
|
||||
mediaPath = func(a *gtsmodel.MediaAttachment) string {
|
||||
return a.File.Path
|
||||
}
|
||||
|
||||
// Thumbnail media size.
|
||||
case media.SizeSmall:
|
||||
content.ContentType = attach.Thumbnail.ContentType
|
||||
content.ContentLength = int64(attach.Thumbnail.FileSize)
|
||||
mediaPath = attach.Thumbnail.Path
|
||||
mediaPath = func(a *gtsmodel.MediaAttachment) string {
|
||||
return a.Thumbnail.Path
|
||||
}
|
||||
|
||||
default:
|
||||
const text = "invalid media size"
|
||||
@@ -213,12 +215,12 @@ func (p *Processor) getAttachmentContent(
|
||||
|
||||
// Check media is meant
|
||||
// to be cached locally.
|
||||
if *attach.Cached {
|
||||
if attach.Cached() {
|
||||
|
||||
// Check storage for media at determined path.
|
||||
rc, err = p.state.Storage.GetStream(ctx, mediaPath)
|
||||
// Check storage for media at determined fileserver path.
|
||||
rc, err = p.state.Storage.GetStream(ctx, mediaPath(attach))
|
||||
if err != nil && !storage.IsNotFound(err) {
|
||||
err := gtserror.Newf("storage error getting media %s: %w", attach.URL, err)
|
||||
err := gtserror.Newf("storage error getting cached media %s: %w", attach.URL, err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
}
|
||||
@@ -227,14 +229,10 @@ func (p *Processor) getAttachmentContent(
|
||||
// This is local media without
|
||||
// a cached attachment, unfulfillable!
|
||||
if attach.IsLocal() {
|
||||
return nil, gtserror.NewfWithCode(http.StatusNotFound,
|
||||
"local media file not found: %s", attach.URL)
|
||||
return nil, gtserror.NewWithCode(http.StatusNotFound,
|
||||
"local media file not found")
|
||||
}
|
||||
|
||||
// Whether the cached flag was set or
|
||||
// not, we know it isn't in storage.
|
||||
attach.Cached = util.Ptr(false)
|
||||
|
||||
// Attempt to recache this remote media.
|
||||
attach, err = p.federator.RefreshMedia(ctx,
|
||||
requestUser,
|
||||
@@ -244,24 +242,22 @@ func (p *Processor) getAttachmentContent(
|
||||
false, // async
|
||||
)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error recaching media %s: %w", attach.URL, err)
|
||||
return nil, gtserror.NewErrorNotFound(err)
|
||||
err := gtserror.Newf("error recaching media %s: %w", attach.RemoteURL, err)
|
||||
return nil, gtserror.WrapWithCode(http.StatusNotFound, err)
|
||||
}
|
||||
|
||||
// Check storage for media at determined path.
|
||||
rc, err = p.state.Storage.GetStream(ctx, mediaPath)
|
||||
if err != nil && !storage.IsNotFound(err) {
|
||||
err := gtserror.Newf("storage error getting media %s: %w", attach.URL, err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
} else if rc == nil {
|
||||
return nil, gtserror.NewfWithCode(http.StatusNotFound,
|
||||
"remote media file not found: %s", attach.URL)
|
||||
// Check storage for media at determined fileserver path.
|
||||
rc, err = p.state.Storage.GetStream(ctx, mediaPath(attach))
|
||||
if err != nil {
|
||||
err := gtserror.Newf("storage error getting recached media %s: %w", attach.RemoteURL, err)
|
||||
return nil, gtserror.WrapWithCode(http.StatusInternalServerError, err)
|
||||
}
|
||||
}
|
||||
|
||||
// If running on S3 storage with proxying disabled,
|
||||
// just fetch a pre-signed URL instead of the content.
|
||||
if url := p.state.Storage.URL(ctx, mediaPath); url != nil {
|
||||
url := p.state.Storage.URL(ctx, mediaPath(attach))
|
||||
if url != nil {
|
||||
_ = rc.Close() // close storage stream
|
||||
content.URL = url
|
||||
return &content, nil
|
||||
@@ -312,20 +308,24 @@ func (p *Processor) getEmojiContent(
|
||||
// Start preparing API content model and other
|
||||
// values depending on requested media size.
|
||||
var content apimodel.Content
|
||||
var emojiPath string
|
||||
var emojiPath func(*gtsmodel.Emoji) string
|
||||
switch sizeStr {
|
||||
|
||||
// Original emoji image.
|
||||
case media.SizeOriginal:
|
||||
content.ContentType = emoji.ImageContentType
|
||||
content.ContentLength = int64(emoji.ImageFileSize)
|
||||
emojiPath = emoji.ImagePath
|
||||
emojiPath = func(e *gtsmodel.Emoji) string {
|
||||
return e.ImagePath
|
||||
}
|
||||
|
||||
// Static emoji image.
|
||||
case media.SizeStatic:
|
||||
content.ContentType = emoji.ImageStaticContentType
|
||||
content.ContentLength = int64(emoji.ImageStaticFileSize)
|
||||
emojiPath = emoji.ImageStaticPath
|
||||
emojiPath = func(e *gtsmodel.Emoji) string {
|
||||
return e.ImageStaticPath
|
||||
}
|
||||
|
||||
default:
|
||||
const text = "invalid emoji size"
|
||||
@@ -341,12 +341,12 @@ func (p *Processor) getEmojiContent(
|
||||
|
||||
// Check emoji is meant
|
||||
// to be cached locally.
|
||||
if *emoji.Cached {
|
||||
if emoji.Cached() {
|
||||
|
||||
// Check storage for emoji at determined image path.
|
||||
rc, err = p.state.Storage.GetStream(ctx, emojiPath)
|
||||
// Check storage for emoji at determined fileserver path.
|
||||
rc, err = p.state.Storage.GetStream(ctx, emojiPath(emoji))
|
||||
if err != nil && !storage.IsNotFound(err) {
|
||||
err := gtserror.Newf("storage error getting emoji %s: %w", emoji.URI, err)
|
||||
err := gtserror.Newf("storage error getting cached emoji %s: %w", emoji.URI, err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
}
|
||||
@@ -355,14 +355,10 @@ func (p *Processor) getEmojiContent(
|
||||
// This is a local emoji without
|
||||
// a cached image, unfulfillable!
|
||||
if emoji.IsLocal() {
|
||||
return nil, gtserror.NewfWithCode(http.StatusNotFound,
|
||||
"local emoji image not found: %s", emoji.URI)
|
||||
return nil, gtserror.NewWithCode(http.StatusNotFound,
|
||||
"local emoji file not found")
|
||||
}
|
||||
|
||||
// Whether the cached flag was set or
|
||||
// not, we know it isn't in storage.
|
||||
emoji.Cached = util.Ptr(false)
|
||||
|
||||
// Attempt to recache this remote emoji.
|
||||
emoji, err = p.federator.RecacheEmoji(ctx,
|
||||
emoji,
|
||||
@@ -371,23 +367,21 @@ func (p *Processor) getEmojiContent(
|
||||
)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error recaching emoji %s: %w", emoji.URI, err)
|
||||
return nil, gtserror.NewErrorNotFound(err)
|
||||
return nil, gtserror.WrapWithCode(http.StatusNotFound, err)
|
||||
}
|
||||
|
||||
// Check storage for emoji at determined image path.
|
||||
rc, err = p.state.Storage.GetStream(ctx, emojiPath)
|
||||
if err != nil && !storage.IsNotFound(err) {
|
||||
err := gtserror.Newf("storage error getting emoji %s after recache: %w", emoji.URI, err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
} else if rc == nil {
|
||||
return nil, gtserror.NewfWithCode(http.StatusNotFound,
|
||||
"remote emoji image not found: %s", emoji.URI)
|
||||
// Check storage for emoji at determined fileserver path.
|
||||
rc, err = p.state.Storage.GetStream(ctx, emojiPath(emoji))
|
||||
if err != nil {
|
||||
err := gtserror.Newf("storage error getting recached emoji %s: %w", emoji.URI, err)
|
||||
return nil, gtserror.WrapWithCode(http.StatusInternalServerError, err)
|
||||
}
|
||||
}
|
||||
|
||||
// If running on S3 storage with proxying disabled,
|
||||
// just fetch a pre-signed URL instead of the content.
|
||||
if url := p.state.Storage.URL(ctx, emojiPath); url != nil {
|
||||
url := p.state.Storage.URL(ctx, emojiPath(emoji))
|
||||
if url != nil {
|
||||
_ = rc.Close() // close storage stream
|
||||
content.URL = url
|
||||
return &content, nil
|
||||
|
||||
@@ -26,7 +26,6 @@ import (
|
||||
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/media"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/util"
|
||||
"code.superseriousbusiness.org/gotosocial/testrig"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
@@ -68,18 +67,21 @@ func (suite *GetFileTestSuite) TestGetRemoteFileUncached() {
|
||||
|
||||
// uncache the file from local
|
||||
testAttachment := suite.testAttachments["remote_account_1_status_1_attachment_1"]
|
||||
testAttachment.Cached = util.Ptr(false)
|
||||
err := suite.db.UpdateByID(ctx, testAttachment, testAttachment.ID, "cached")
|
||||
suite.NoError(err)
|
||||
err = suite.storage.Delete(ctx, testAttachment.File.Path)
|
||||
|
||||
err := suite.storage.Delete(ctx, testAttachment.File.Path)
|
||||
suite.NoError(err)
|
||||
err = suite.storage.Delete(ctx, testAttachment.Thumbnail.Path)
|
||||
suite.NoError(err)
|
||||
|
||||
// now fetch it
|
||||
fileName := path.Base(testAttachment.File.Path)
|
||||
requestingAccount := suite.testAccounts["local_account_1"]
|
||||
testAttachment.File.Path = ""
|
||||
testAttachment.Thumbnail.Path = ""
|
||||
|
||||
err = suite.db.UpdateAttachment(ctx, testAttachment, "thumbnail_path", "file_path")
|
||||
suite.NoError(err)
|
||||
|
||||
// now fetch it
|
||||
requestingAccount := suite.testAccounts["local_account_1"]
|
||||
content, errWithCode := suite.mediaProcessor.GetFile(ctx, requestingAccount, &apimodel.GetContentRequestForm{
|
||||
AccountID: testAttachment.AccountID,
|
||||
MediaType: string(media.TypeAttachment),
|
||||
@@ -107,7 +109,7 @@ func (suite *GetFileTestSuite) TestGetRemoteFileUncached() {
|
||||
}
|
||||
|
||||
suite.NoError(err)
|
||||
suite.True(*dbAttachment.Cached)
|
||||
suite.True(dbAttachment.Cached())
|
||||
|
||||
// the file should be back in storage at the same path as before
|
||||
refreshedBytes, err := suite.storage.Get(ctx, dbAttachment.File.Path)
|
||||
@@ -120,18 +122,21 @@ func (suite *GetFileTestSuite) TestGetRemoteFileUncachedInterrupted() {
|
||||
|
||||
// uncache the file from local
|
||||
testAttachment := suite.testAttachments["remote_account_1_status_1_attachment_1"]
|
||||
testAttachment.Cached = util.Ptr(false)
|
||||
err := suite.db.UpdateByID(ctx, testAttachment, testAttachment.ID, "cached")
|
||||
suite.NoError(err)
|
||||
err = suite.storage.Delete(ctx, testAttachment.File.Path)
|
||||
|
||||
err := suite.storage.Delete(ctx, testAttachment.File.Path)
|
||||
suite.NoError(err)
|
||||
err = suite.storage.Delete(ctx, testAttachment.Thumbnail.Path)
|
||||
suite.NoError(err)
|
||||
|
||||
// now fetch it
|
||||
fileName := path.Base(testAttachment.File.Path)
|
||||
requestingAccount := suite.testAccounts["local_account_1"]
|
||||
testAttachment.File.Path = ""
|
||||
testAttachment.Thumbnail.Path = ""
|
||||
|
||||
err = suite.db.UpdateAttachment(ctx, testAttachment, "thumbnail_path", "file_path")
|
||||
suite.NoError(err)
|
||||
|
||||
// now fetch it
|
||||
requestingAccount := suite.testAccounts["local_account_1"]
|
||||
content, errWithCode := suite.mediaProcessor.GetFile(ctx, requestingAccount, &apimodel.GetContentRequestForm{
|
||||
AccountID: testAttachment.AccountID,
|
||||
MediaType: string(media.TypeAttachment),
|
||||
@@ -151,7 +156,7 @@ func (suite *GetFileTestSuite) TestGetRemoteFileUncachedInterrupted() {
|
||||
var dbAttachment *gtsmodel.MediaAttachment
|
||||
if !testrig.WaitFor(func() bool {
|
||||
dbAttachment, _ = suite.db.GetAttachmentByID(ctx, testAttachment.ID)
|
||||
return *dbAttachment.Cached
|
||||
return dbAttachment.Cached()
|
||||
}) {
|
||||
suite.FailNow("timed out waiting for attachment to be updated")
|
||||
}
|
||||
@@ -171,18 +176,20 @@ func (suite *GetFileTestSuite) TestGetRemoteFileThumbnailUncached() {
|
||||
suite.NoError(err)
|
||||
|
||||
// uncache the file from local
|
||||
testAttachment.Cached = util.Ptr(false)
|
||||
err = suite.db.UpdateByID(ctx, testAttachment, testAttachment.ID, "cached")
|
||||
suite.NoError(err)
|
||||
err = suite.storage.Delete(ctx, testAttachment.File.Path)
|
||||
suite.NoError(err)
|
||||
err = suite.storage.Delete(ctx, testAttachment.Thumbnail.Path)
|
||||
suite.NoError(err)
|
||||
|
||||
// now fetch the thumbnail
|
||||
fileName := path.Base(testAttachment.File.Path)
|
||||
requestingAccount := suite.testAccounts["local_account_1"]
|
||||
testAttachment.File.Path = ""
|
||||
testAttachment.Thumbnail.Path = ""
|
||||
|
||||
err = suite.db.UpdateAttachment(ctx, testAttachment, "thumbnail_path", "file_path")
|
||||
suite.NoError(err)
|
||||
|
||||
// now fetch the thumbnail
|
||||
requestingAccount := suite.testAccounts["local_account_1"]
|
||||
content, errWithCode := suite.mediaProcessor.GetFile(ctx, requestingAccount, &apimodel.GetContentRequestForm{
|
||||
AccountID: testAttachment.AccountID,
|
||||
MediaType: string(media.TypeAttachment),
|
||||
|
||||
@@ -59,17 +59,6 @@ type MediaStandardTestSuite struct {
|
||||
mediaProcessor mediaprocessing.Processor
|
||||
}
|
||||
|
||||
func (suite *MediaStandardTestSuite) SetupSuite() {
|
||||
suite.testTokens = testrig.NewTestTokens()
|
||||
suite.testApplications = testrig.NewTestApplications()
|
||||
suite.testUsers = testrig.NewTestUsers()
|
||||
suite.testAccounts = testrig.NewTestAccounts()
|
||||
suite.testAttachments = testrig.NewTestAttachments()
|
||||
suite.testStatuses = testrig.NewTestStatuses()
|
||||
suite.testRemoteAttachments = testrig.NewTestFediAttachments("../../../testrig/media")
|
||||
suite.testDomainLimits = testrig.NewTestDomainLimits()
|
||||
}
|
||||
|
||||
func (suite *MediaStandardTestSuite) SetupTest() {
|
||||
suite.state.Caches.Init()
|
||||
|
||||
@@ -94,6 +83,15 @@ func (suite *MediaStandardTestSuite) SetupTest() {
|
||||
suite.mediaProcessor = mediaprocessing.New(&common, &suite.state, suite.tc, federator, suite.mediaManager, suite.transportController)
|
||||
testrig.StandardDBSetup(suite.db, nil)
|
||||
testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
|
||||
|
||||
suite.testTokens = testrig.NewTestTokens()
|
||||
suite.testApplications = testrig.NewTestApplications()
|
||||
suite.testUsers = testrig.NewTestUsers()
|
||||
suite.testAccounts = testrig.NewTestAccounts()
|
||||
suite.testAttachments = testrig.NewTestAttachments()
|
||||
suite.testStatuses = testrig.NewTestStatuses()
|
||||
suite.testRemoteAttachments = testrig.NewTestFediAttachments("../../../testrig/media")
|
||||
suite.testDomainLimits = testrig.NewTestDomainLimits()
|
||||
}
|
||||
|
||||
func (suite *MediaStandardTestSuite) TearDownTest() {
|
||||
|
||||
@@ -684,7 +684,20 @@ func AttachmentToAPIAttachment(media *gtsmodel.MediaAttachment) apimodel.Attachm
|
||||
api.Type = media.Type.String()
|
||||
api.ID = media.ID
|
||||
|
||||
if media.File.Path != "" {
|
||||
// Set initial API model attachment fields.
|
||||
api.Blurhash = util.PtrIf(media.Blurhash)
|
||||
api.RemoteURL = util.PtrIf(media.RemoteURL)
|
||||
api.PreviewRemoteURL = util.PtrIf(media.Thumbnail.RemoteURL)
|
||||
api.Description = util.PtrIf(media.Description)
|
||||
|
||||
if media.Error != 0 {
|
||||
// Set media error string.
|
||||
api.Error = new(string)
|
||||
*api.Error = media.Error.String()
|
||||
return api
|
||||
}
|
||||
|
||||
if media.URL != "" {
|
||||
// Allocate media metadata object.
|
||||
api.Meta = new(apimodel.MediaMeta)
|
||||
|
||||
@@ -699,7 +712,7 @@ func AttachmentToAPIAttachment(media *gtsmodel.MediaAttachment) apimodel.Attachm
|
||||
// If the URL is set, either the file is currently
|
||||
// processing, or is successfully stored locally.
|
||||
api.TextURL = util.Ptr(media.URL)
|
||||
api.URL = util.Ptr(media.URL)
|
||||
api.URL = api.TextURL
|
||||
|
||||
// Only add file details if we have any stored.
|
||||
if media.FileMeta.Original != zeroOriginal {
|
||||
@@ -715,7 +728,7 @@ func AttachmentToAPIAttachment(media *gtsmodel.MediaAttachment) apimodel.Attachm
|
||||
}
|
||||
}
|
||||
|
||||
if media.Thumbnail.Path != "" {
|
||||
if media.Thumbnail.URL != "" {
|
||||
if api.Meta == nil {
|
||||
// Allocate media metadata object.
|
||||
api.Meta = new(apimodel.MediaMeta)
|
||||
@@ -744,12 +757,6 @@ func AttachmentToAPIAttachment(media *gtsmodel.MediaAttachment) apimodel.Attachm
|
||||
}
|
||||
}
|
||||
|
||||
// Set remaining API attachment fields.
|
||||
api.Blurhash = util.PtrIf(media.Blurhash)
|
||||
api.RemoteURL = util.PtrIf(media.RemoteURL)
|
||||
api.PreviewRemoteURL = util.PtrIf(media.Thumbnail.RemoteURL)
|
||||
api.Description = util.PtrIf(media.Description)
|
||||
|
||||
return api
|
||||
}
|
||||
|
||||
@@ -905,28 +912,28 @@ func (c *Converter) statusToAPIStatus(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert author to API model.
|
||||
acct, err := c.AccountToAPIAccountPublic(ctx, status.Account)
|
||||
// Convert status author account to frontend API model.
|
||||
apiStatus.Account, err = c.AccountToAPIAccountPublic(ctx,
|
||||
status.Account)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error converting status acct: %w", err)
|
||||
return nil, gtserror.Newf("error converting status author: %w", err)
|
||||
}
|
||||
apiStatus.Account = acct
|
||||
|
||||
// Convert author of boosted
|
||||
// status (if set) to API model.
|
||||
if apiStatus.Reblog != nil {
|
||||
boostAcct, err := c.AccountToAPIAccountPublic(ctx, status.BoostOfAccount)
|
||||
apiStatus.Reblog.Account, err = c.AccountToAPIAccountPublic(ctx, status.BoostOfAccount)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error converting boost acct: %w", err)
|
||||
return nil, gtserror.Newf("error converting boost author: %w", err)
|
||||
}
|
||||
apiStatus.Reblog.Account = boostAcct
|
||||
}
|
||||
|
||||
if placeholdAttachments {
|
||||
var attachNote string
|
||||
|
||||
// Normalize status for API by pruning attachments
|
||||
// that were not able to be locally stored, and replacing
|
||||
// them with a helpful message + links to remote.
|
||||
var attachNote string
|
||||
attachNote, apiStatus.MediaAttachments = placeholderAttachments(apiStatus.MediaAttachments)
|
||||
apiStatus.Content += attachNote
|
||||
|
||||
@@ -949,7 +956,6 @@ func (c *Converter) statusToAPIStatus(
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error deriving 'pending reply' note: %w", err)
|
||||
}
|
||||
|
||||
apiStatus.Content += pendingNote
|
||||
}
|
||||
}
|
||||
@@ -1331,7 +1337,7 @@ func (c *Converter) baseStatusToFrontend(
|
||||
|
||||
apiStatus.Poll, err = c.PollToAPIPoll(ctx, requester, poll)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error converting poll: %w", err)
|
||||
return nil, gtserror.Newf("error converting poll: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1347,9 +1353,6 @@ func (c *Converter) baseStatusToFrontend(
|
||||
"error getting interactions for status %s for account %s: %v",
|
||||
status.URI, requester.URI, err,
|
||||
)
|
||||
|
||||
// Ensure non-nil object.
|
||||
interacts = new(statusInteractions)
|
||||
}
|
||||
apiStatus.Favourited = interacts.Favourited
|
||||
apiStatus.Bookmarked = interacts.Bookmarked
|
||||
@@ -2942,159 +2945,6 @@ func (c *Converter) ScheduledStatusToAPIScheduledStatus(ctx context.Context, sta
|
||||
return apiScheduledStatus, nil
|
||||
}
|
||||
|
||||
// attachmentsToAPI converts database model media attachments (fetching
|
||||
// using IDs if necessary) to frontend API attachment models. all errors
|
||||
// are caught and logged, with the calling function name as a prefix.
|
||||
func (c *Converter) attachmentsToAPI(
|
||||
ctx context.Context,
|
||||
attachments []*gtsmodel.MediaAttachment,
|
||||
attachmentIDs []string,
|
||||
) []*apimodel.Attachment {
|
||||
caller := log.Caller(3)
|
||||
|
||||
// Check if media attachments are populated.
|
||||
if len(attachments) != len(attachmentIDs) {
|
||||
var err error
|
||||
|
||||
// Media attachments are not populated, fetch from the database.
|
||||
attachments, err = c.state.DB.GetAttachmentsByIDs(ctx, attachmentIDs)
|
||||
if err != nil {
|
||||
|
||||
log.Errorf(ctx, "%s: error getting media: %v", caller, err)
|
||||
return []*apimodel.Attachment{}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert all db media attachments to slice of API models.
|
||||
apiModels := make([]*apimodel.Attachment, len(attachments))
|
||||
if len(apiModels) != len(attachments) {
|
||||
panic(gtserror.New("bound check elimination"))
|
||||
}
|
||||
for i, media := range attachments {
|
||||
apiModel := AttachmentToAPIAttachment(media)
|
||||
apiModels[i] = &apiModel
|
||||
}
|
||||
|
||||
return apiModels
|
||||
}
|
||||
|
||||
// emojisToAPI converts database model emojis (fetching using IDs if
|
||||
// necessary) to frontend API emoji models. all errors are caught and
|
||||
// logged, with the calling function name as a prefix.
|
||||
func (c *Converter) emojisToAPI(
|
||||
ctx context.Context,
|
||||
emojis []*gtsmodel.Emoji,
|
||||
emojiIDs []string,
|
||||
) []apimodel.Emoji {
|
||||
caller := log.Caller(3)
|
||||
|
||||
// Check if emojis are populated.
|
||||
if len(emojis) != len(emojiIDs) {
|
||||
var err error
|
||||
|
||||
// Emojis are not populated, fetch from the database.
|
||||
emojis, err = c.state.DB.GetEmojisByIDs(ctx, emojiIDs)
|
||||
if err != nil {
|
||||
|
||||
log.Errorf(ctx, "%s: error getting emojis: %v", caller, err)
|
||||
return []apimodel.Emoji{}
|
||||
}
|
||||
}
|
||||
|
||||
// Preallocate a biggest-case slice of frontend emojis.
|
||||
apiModels := make([]apimodel.Emoji, 0, len(emojis))
|
||||
for _, emoji := range emojis {
|
||||
|
||||
// Convert each database emoji to API model.
|
||||
apiModel, err := c.EmojiToAPIEmoji(ctx, emoji)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "%s: error converting emoji %s: %v", caller, emoji.ShortcodeDomain(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Append API model to the return slice.
|
||||
apiModels = append(apiModels, apiModel)
|
||||
}
|
||||
|
||||
return apiModels
|
||||
}
|
||||
|
||||
// mentionsToAPI converts database model mentions (fetching using IDs if
|
||||
// necessary) to frontend API mention models. all errors are caught and
|
||||
// logged, with the calling function name as a prefix.
|
||||
func (c *Converter) mentionsToAPI(
|
||||
ctx context.Context,
|
||||
mentions []*gtsmodel.Mention,
|
||||
mentionIDs []string,
|
||||
) []apimodel.Mention {
|
||||
caller := log.Caller(3)
|
||||
|
||||
// Check if mentions are populated.
|
||||
if len(mentions) != len(mentionIDs) {
|
||||
var err error
|
||||
|
||||
// Mentions are not populated, fetch from the database.
|
||||
mentions, err = c.state.DB.GetMentions(ctx, mentionIDs)
|
||||
if err != nil {
|
||||
|
||||
log.Errorf(ctx, "%s: error getting mentions: %v", caller, err)
|
||||
return []apimodel.Mention{}
|
||||
}
|
||||
}
|
||||
|
||||
// Preallocate a biggest-case slice of frontend mentions.
|
||||
apiModels := make([]apimodel.Mention, 0, len(mentions))
|
||||
for _, mention := range mentions {
|
||||
|
||||
// Convert each database mention to frontend API model.
|
||||
apiModel, err := c.MentionToAPIMention(ctx, mention)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "%s: error converting mention %s: %v", caller, mention.ID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Append API model to the return slice.
|
||||
apiModels = append(apiModels, apiModel)
|
||||
}
|
||||
|
||||
return apiModels
|
||||
}
|
||||
|
||||
// tagsToAPI converts database model tags (fetching using IDs if
|
||||
// necessary) to frontend API tag models. all errors are caught
|
||||
// and logged, with the calling function name as a prefix.
|
||||
func (c *Converter) tagsToAPI(
|
||||
ctx context.Context,
|
||||
tags []*gtsmodel.Tag,
|
||||
tagIDs []string,
|
||||
) []apimodel.Tag {
|
||||
caller := log.Caller(3)
|
||||
|
||||
// Check if mentions are populated.
|
||||
if len(tags) != len(tagIDs) {
|
||||
var err error
|
||||
|
||||
// Tags not populated, fetch from database.
|
||||
tags, err = c.state.DB.GetTags(ctx, tagIDs)
|
||||
if err != nil {
|
||||
|
||||
log.Errorf(ctx, "%s: error getting tags: %v", caller, err)
|
||||
return []apimodel.Tag{}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert all db tags to slice of API models.
|
||||
apiModels := make([]apimodel.Tag, len(tags))
|
||||
if len(apiModels) != len(tags) {
|
||||
panic(gtserror.New("bound check elimination"))
|
||||
}
|
||||
for i, tag := range tags {
|
||||
apiModels[i] = TagToAPITag(tag, false, nil)
|
||||
}
|
||||
|
||||
return apiModels
|
||||
}
|
||||
|
||||
func (c *Converter) DomainLimitToAPIDomainLimit(
|
||||
ctx context.Context,
|
||||
domainLimit *gtsmodel.DomainLimit,
|
||||
@@ -3221,3 +3071,152 @@ func domainLimitStatusesPolicyToFilterAction(p gtsmodel.StatusesPolicy) apimodel
|
||||
return apimodel.FilterActionNone
|
||||
}
|
||||
}
|
||||
|
||||
// attachmentsToAPI converts database model media attachments (fetching
|
||||
// using IDs if necessary) to frontend API attachment models. all errors
|
||||
// are caught and logged, with the calling function name as a prefix.
|
||||
func (c *Converter) attachmentsToAPI(
|
||||
ctx context.Context,
|
||||
attachments []*gtsmodel.MediaAttachment,
|
||||
attachmentIDs []string,
|
||||
) []*apimodel.Attachment {
|
||||
|
||||
// Check if media attachments are populated.
|
||||
if len(attachments) != len(attachmentIDs) {
|
||||
var err error
|
||||
|
||||
// Media attachments are not populated, fetch from the database.
|
||||
attachments, err = c.state.DB.GetAttachmentsByIDs(ctx, attachmentIDs)
|
||||
if err != nil {
|
||||
|
||||
log.Error(ctx, gtserror.NewfAt(3, "error getting media: %w", err))
|
||||
return []*apimodel.Attachment{}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert all db media attachments to slice of API models.
|
||||
apiModels := make([]*apimodel.Attachment, len(attachments))
|
||||
if len(apiModels) != len(attachments) {
|
||||
panic(gtserror.New("bound check elimination"))
|
||||
}
|
||||
for i, media := range attachments {
|
||||
apiModel := AttachmentToAPIAttachment(media)
|
||||
apiModels[i] = &apiModel
|
||||
}
|
||||
|
||||
return apiModels
|
||||
}
|
||||
|
||||
// emojisToAPI converts database model emojis (fetching using IDs if
|
||||
// necessary) to frontend API emoji models. all errors are caught and
|
||||
// logged, with the calling function name as a prefix.
|
||||
func (c *Converter) emojisToAPI(
|
||||
ctx context.Context,
|
||||
emojis []*gtsmodel.Emoji,
|
||||
emojiIDs []string,
|
||||
) []apimodel.Emoji {
|
||||
|
||||
// Check if emojis are populated.
|
||||
if len(emojis) != len(emojiIDs) {
|
||||
var err error
|
||||
|
||||
// Emojis are not populated, fetch from the database.
|
||||
emojis, err = c.state.DB.GetEmojisByIDs(ctx, emojiIDs)
|
||||
if err != nil {
|
||||
|
||||
log.Error(ctx, gtserror.NewfAt(3, "error getting emojis: %w", err))
|
||||
return []apimodel.Emoji{}
|
||||
}
|
||||
}
|
||||
|
||||
// Preallocate a biggest-case slice of frontend emojis.
|
||||
apiModels := make([]apimodel.Emoji, 0, len(emojis))
|
||||
for _, emoji := range emojis {
|
||||
|
||||
// Convert each database emoji to API model.
|
||||
apiModel, err := c.EmojiToAPIEmoji(ctx, emoji)
|
||||
if err != nil {
|
||||
log.Error(ctx, gtserror.NewfAt(3, "error converting emoji %s: %w", emoji.ShortcodeDomain(), err))
|
||||
continue
|
||||
}
|
||||
|
||||
// Append API model to the return slice.
|
||||
apiModels = append(apiModels, apiModel)
|
||||
}
|
||||
|
||||
return apiModels
|
||||
}
|
||||
|
||||
// mentionsToAPI converts database model mentions (fetching using IDs if
|
||||
// necessary) to frontend API mention models. all errors are caught and
|
||||
// logged, with the calling function name as a prefix.
|
||||
func (c *Converter) mentionsToAPI(
|
||||
ctx context.Context,
|
||||
mentions []*gtsmodel.Mention,
|
||||
mentionIDs []string,
|
||||
) []apimodel.Mention {
|
||||
|
||||
// Check if mentions are populated.
|
||||
if len(mentions) != len(mentionIDs) {
|
||||
var err error
|
||||
|
||||
// Mentions are not populated, fetch from the database.
|
||||
mentions, err = c.state.DB.GetMentions(ctx, mentionIDs)
|
||||
if err != nil {
|
||||
|
||||
log.Error(ctx, gtserror.NewfAt(3, "error getting mentions: %w", err))
|
||||
return []apimodel.Mention{}
|
||||
}
|
||||
}
|
||||
|
||||
// Preallocate a biggest-case slice of frontend mentions.
|
||||
apiModels := make([]apimodel.Mention, 0, len(mentions))
|
||||
for _, mention := range mentions {
|
||||
|
||||
// Convert each database mention to frontend API model.
|
||||
apiModel, err := c.MentionToAPIMention(ctx, mention)
|
||||
if err != nil {
|
||||
log.Error(ctx, gtserror.NewfAt(3, "error converting mention %s: %w", mention.ID, err))
|
||||
continue
|
||||
}
|
||||
|
||||
// Append API model to the return slice.
|
||||
apiModels = append(apiModels, apiModel)
|
||||
}
|
||||
|
||||
return apiModels
|
||||
}
|
||||
|
||||
// tagsToAPI converts database model tags (fetching using IDs if
|
||||
// necessary) to frontend API tag models. all errors are caught
|
||||
// and logged, with the calling function name as a prefix.
|
||||
func (c *Converter) tagsToAPI(
|
||||
ctx context.Context,
|
||||
tags []*gtsmodel.Tag,
|
||||
tagIDs []string,
|
||||
) []apimodel.Tag {
|
||||
|
||||
// Check if mentions are populated.
|
||||
if len(tags) != len(tagIDs) {
|
||||
var err error
|
||||
|
||||
// Tags not populated, fetch from database.
|
||||
tags, err = c.state.DB.GetTags(ctx, tagIDs)
|
||||
if err != nil {
|
||||
|
||||
log.Error(ctx, gtserror.NewfAt(3, "error getting tags: %w", err))
|
||||
return []apimodel.Tag{}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert all db tags to slice of API models.
|
||||
apiModels := make([]apimodel.Tag, len(tags))
|
||||
if len(apiModels) != len(tags) {
|
||||
panic(gtserror.New("bound check elimination"))
|
||||
}
|
||||
for i, tag := range tags {
|
||||
apiModels[i] = TagToAPITag(tag, false, nil)
|
||||
}
|
||||
|
||||
return apiModels
|
||||
}
|
||||
|
||||
@@ -1002,7 +1002,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendUnknownAttachments
|
||||
"muted": false,
|
||||
"bookmarked": false,
|
||||
"pinned": false,
|
||||
"content": "\u003cp\u003ehi \u003cspan class=\"h-card\"\u003e\u003ca href=\"http://localhost:8080/@admin\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003e@\u003cspan\u003eadmin\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e here's some media for ya\u003c/p\u003e\u003cdiv class=\"gts-system-message gts-placeholder-attachments\"\u003e\u003chr\u003e\u003cp\u003e\u003ci lang=\"en\"\u003eℹ️ Note from localhost:8080: 2 attachments in this status were not downloaded. Treat the following external links with care:\u003c/i\u003e\u003c/p\u003e\u003cul\u003e\u003cli\u003e\u003ca href=\"http://example.org/fileserver/01HE7Y659ZWZ02JM4AWYJZ176Q/attachment/original/01HE7ZGJYTSYMXF927GF9353KR.svg\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003e01HE7ZGJYTSYMXF927GF9353KR.svg\u003c/a\u003e [SVG line art of a sloth, public domain]\u003c/li\u003e\u003cli\u003e\u003ca href=\"http://example.org/fileserver/01HE7Y659ZWZ02JM4AWYJZ176Q/attachment/original/01HE892Y8ZS68TQCNPX7J888P3.mp3\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003e01HE892Y8ZS68TQCNPX7J888P3.mp3\u003c/a\u003e [Jolly salsa song, public domain.]\u003c/li\u003e\u003c/ul\u003e\u003c/div\u003e",
|
||||
"content": "\u003cp\u003ehi \u003cspan class=\"h-card\"\u003e\u003ca href=\"http://localhost:8080/@admin\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003e@\u003cspan\u003eadmin\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e here's some media for ya\u003c/p\u003e\u003cdiv class=\"gts-system-message gts-placeholder-attachments\"\u003e\u003chr\u003e\u003cp\u003e\u003ci lang=\"en\"\u003eℹ️ Note from localhost:8080: 2 attachments in this status were not downloaded. Treat the following external links with care:\u003c/i\u003e\u003c/p\u003e\u003cul\u003e\u003cli\u003e\u003ca href=\"http://example.org/fileserver/01HE7Y659ZWZ02JM4AWYJZ176Q/attachment/original/01HE7ZGJYTSYMXF927GF9353KR.svg\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003e01HE7ZGJYTSYMXF927GF9353KR.svg\u003c/a\u003e [SVG line art of a sloth, public domain] (error: unsupported media type)\u003c/li\u003e\u003cli\u003e\u003ca href=\"http://example.org/fileserver/01HE7Y659ZWZ02JM4AWYJZ176Q/attachment/original/01HE892Y8ZS68TQCNPX7J888P3.mp3\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003e01HE892Y8ZS68TQCNPX7J888P3.mp3\u003c/a\u003e [Jolly salsa song, public domain.] (error: unsupported media type)\u003c/li\u003e\u003c/ul\u003e\u003c/div\u003e",
|
||||
"reblog": null,
|
||||
"account": {
|
||||
"id": "01FHMQX3GAABWSM0S2VZEC2SWC",
|
||||
@@ -1270,6 +1270,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
|
||||
"meta": null,
|
||||
"description": "SVG line art of a sloth, public domain",
|
||||
"blurhash": "L26*j+~qE1RP?wxut7ofRlM{R*of",
|
||||
"error": "unsupported media type",
|
||||
"Sensitive": true,
|
||||
"MIMEType": "",
|
||||
"PreviewMIMEType": "",
|
||||
@@ -1286,6 +1287,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
|
||||
"meta": null,
|
||||
"description": "Jolly salsa song, public domain.",
|
||||
"blurhash": null,
|
||||
"error": "unsupported media type",
|
||||
"Sensitive": true,
|
||||
"MIMEType": "",
|
||||
"PreviewMIMEType": "",
|
||||
@@ -3880,7 +3882,8 @@ func (suite *InternalToFrontendTestSuite) TestStatusToAPIEdits() {
|
||||
"preview_remote_url": null,
|
||||
"meta": null,
|
||||
"description": "Jolly salsa song, public domain.",
|
||||
"blurhash": null
|
||||
"blurhash": null,
|
||||
"error": "unsupported media type"
|
||||
}
|
||||
],
|
||||
"emojis": []
|
||||
@@ -3987,7 +3990,8 @@ func (suite *InternalToFrontendTestSuite) TestStatusToAPIEdits() {
|
||||
"preview_remote_url": null,
|
||||
"meta": null,
|
||||
"description": "Jolly salsa song, public domain.",
|
||||
"blurhash": null
|
||||
"blurhash": null,
|
||||
"error": "unsupported media type"
|
||||
}
|
||||
],
|
||||
"emojis": []
|
||||
|
||||
+14
-18
@@ -20,7 +20,6 @@ package typeutils
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/url"
|
||||
"path"
|
||||
@@ -67,33 +66,27 @@ type statusInteractions struct {
|
||||
Pinned bool
|
||||
}
|
||||
|
||||
func (c *Converter) interactionsWithStatusForAccount(ctx context.Context, s *gtsmodel.Status, requestingAccount *gtsmodel.Account) (*statusInteractions, error) {
|
||||
si := &statusInteractions{}
|
||||
|
||||
func (c *Converter) interactionsWithStatusForAccount(ctx context.Context, s *gtsmodel.Status, requestingAccount *gtsmodel.Account) (si statusInteractions, err error) {
|
||||
if requestingAccount != nil {
|
||||
faved, err := c.state.DB.IsStatusFavedBy(ctx, s.ID, requestingAccount.ID)
|
||||
si.Favourited, err = c.state.DB.IsStatusFavedBy(ctx, s.ID, requestingAccount.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error checking if requesting account has faved status: %s", err)
|
||||
return si, gtserror.Newf("error checking if requesting account has faved status: %s", err)
|
||||
}
|
||||
si.Favourited = faved
|
||||
|
||||
reblogged, err := c.state.DB.IsStatusBoostedBy(ctx, s.ID, requestingAccount.ID)
|
||||
si.Reblogged, err = c.state.DB.IsStatusBoostedBy(ctx, s.ID, requestingAccount.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error checking if requesting account has reblogged status: %s", err)
|
||||
return si, gtserror.Newf("error checking if requesting account has reblogged status: %s", err)
|
||||
}
|
||||
si.Reblogged = reblogged
|
||||
|
||||
muted, err := c.state.DB.IsThreadMutedByAccount(ctx, s.ThreadID, requestingAccount.ID)
|
||||
si.Muted, err = c.state.DB.IsThreadMutedByAccount(ctx, s.ThreadID, requestingAccount.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error checking if requesting account has muted status: %s", err)
|
||||
return si, gtserror.Newf("error checking if requesting account has muted status: %s", err)
|
||||
}
|
||||
si.Muted = muted
|
||||
|
||||
bookmarked, err := c.state.DB.IsStatusBookmarkedBy(ctx, requestingAccount.ID, s.ID)
|
||||
si.Bookmarked, err = c.state.DB.IsStatusBookmarkedBy(ctx, requestingAccount.ID, s.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error checking if requesting account has bookmarked status: %s", err)
|
||||
return si, gtserror.Newf("error checking if requesting account has bookmarked status: %s", err)
|
||||
}
|
||||
si.Bookmarked = bookmarked
|
||||
|
||||
// The only time 'pinned' should be true is if the
|
||||
// requesting account is looking at its OWN status.
|
||||
@@ -140,10 +133,10 @@ func placeholderAttachments(arr []*apimodel.Attachment) (string, []*apimodel.Att
|
||||
|
||||
// Extract non-locally stored attachments into a new slice,
|
||||
// deleting them from input slice. This checks whether any
|
||||
// file metadata has been set, which will only happen in the
|
||||
// download error has been set, which will only happen in the
|
||||
// case that we successfully finish processing an attachment.
|
||||
arr = slices.DeleteFunc(arr, func(elem *apimodel.Attachment) bool {
|
||||
if elem.Meta == nil {
|
||||
if elem.Error != nil {
|
||||
nonLocal = append(nonLocal, elem)
|
||||
return true
|
||||
}
|
||||
@@ -186,6 +179,9 @@ func placeholderAttachments(arr []*apimodel.Attachment) (string, []*apimodel.Att
|
||||
note.WriteString(*d)
|
||||
note.WriteString(`]`)
|
||||
}
|
||||
note.WriteString(` (error: `)
|
||||
note.WriteString(*a.Error)
|
||||
note.WriteString(`)`)
|
||||
note.WriteString(`</li>`)
|
||||
}
|
||||
note.WriteString(`</ul>`)
|
||||
|
||||
+9
-60
@@ -555,7 +555,7 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
|
||||
PrivateKey: &rsa.PrivateKey{},
|
||||
PublicKey: &rsa.PublicKey{},
|
||||
PublicKeyURI: "http://thequeenisstillalive.technology/users/her_fuckin_maj#main-key",
|
||||
HeaderMediaAttachmentID: "01PFPMWK2FF0D9WMHEJHR07C3R",
|
||||
HeaderMediaAttachmentID: "01G549FP8065NKWBPTWHP6Y3PD",
|
||||
HidesToPublicFromUnauthedWeb: util.Ptr(false),
|
||||
HidesCcPublicFromUnauthedWeb: util.Ptr(true),
|
||||
},
|
||||
@@ -745,7 +745,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
Description: "Black and white image of some 50's style text saying: Welcome On Board",
|
||||
ScheduledStatusID: "",
|
||||
Blurhash: "LIIE|gRj00WB-;j[t7j[4nWBj[Rj",
|
||||
Processing: 2,
|
||||
File: gtsmodel.File{
|
||||
Path: "01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpeg",
|
||||
ContentType: "image/jpeg",
|
||||
@@ -760,7 +759,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(true),
|
||||
},
|
||||
"local_account_1_status_4_attachment_1": {
|
||||
ID: "01F8MH7TDVANYKWVE8VVKFPJTJ",
|
||||
@@ -791,7 +789,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
Description: "90's Trent Reznor turning to the camera",
|
||||
ScheduledStatusID: "",
|
||||
Blurhash: "LCDRH758KOxsEMNxENEM9]}?aKxZ",
|
||||
Processing: 2,
|
||||
File: gtsmodel.File{
|
||||
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01F8MH7TDVANYKWVE8VVKFPJTJ.gif",
|
||||
ContentType: "image/gif",
|
||||
@@ -806,7 +803,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(true),
|
||||
},
|
||||
"local_account_1_status_4_attachment_2": {
|
||||
ID: "01CDR64G398ADCHXK08WWTHEZ5",
|
||||
@@ -840,7 +836,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
Description: "A cow adorably licking another cow!",
|
||||
ScheduledStatusID: "",
|
||||
Blurhash: "L9B|BBY8yZtS~AxZV@t6,njEjZV@",
|
||||
Processing: 2,
|
||||
File: gtsmodel.File{
|
||||
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01CDR64G398ADCHXK08WWTHEZ5.mp4",
|
||||
ContentType: "video/mp4",
|
||||
@@ -855,7 +850,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(true),
|
||||
},
|
||||
"local_account_1_unattached_1": {
|
||||
ID: "01F8MH8RMYQ6MSNY3JM2XT1CQ5",
|
||||
@@ -886,7 +880,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
Description: "the oh you meme",
|
||||
ScheduledStatusID: "",
|
||||
Blurhash: "LNABP8o#Dge,S6M}axxVEQjYxWbH",
|
||||
Processing: 2,
|
||||
File: gtsmodel.File{
|
||||
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01F8MH8RMYQ6MSNY3JM2XT1CQ5.jpg",
|
||||
ContentType: "image/jpeg",
|
||||
@@ -901,7 +894,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(true),
|
||||
},
|
||||
"local_account_1_avatar": {
|
||||
ID: "01F8MH58A357CV5K7R7TJMSH6S",
|
||||
@@ -932,7 +924,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
Description: "a green goblin looking nasty",
|
||||
ScheduledStatusID: "",
|
||||
Blurhash: "LHI:dk=G|rj]H[J-5roJvnr@Opag",
|
||||
Processing: 2,
|
||||
File: gtsmodel.File{
|
||||
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpeg",
|
||||
ContentType: "image/jpeg",
|
||||
@@ -947,7 +938,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
},
|
||||
Avatar: util.Ptr(true),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(true),
|
||||
},
|
||||
"local_account_1_header": {
|
||||
ID: "01PFPMWK2FF0D9WMHEJHR07C3Q",
|
||||
@@ -978,7 +968,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
Description: "A very old-school screenshot of the original team fortress mod for quake",
|
||||
ScheduledStatusID: "",
|
||||
Blurhash: "L17KPDs:$ykDJroJ-RoJ0fR+xVjY",
|
||||
Processing: 2,
|
||||
File: gtsmodel.File{
|
||||
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpeg",
|
||||
ContentType: "image/jpeg",
|
||||
@@ -993,7 +982,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(true),
|
||||
Cached: util.Ptr(true),
|
||||
},
|
||||
"local_account_1_status_8_attachment_1": {
|
||||
ID: "01J2M20K6K9XQC4WSB961YJHV6",
|
||||
@@ -1027,7 +1015,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
Description: "This is a track from Nine Inch Nails' \"Ghosts I-V\" album.\n\nThis is the third track from \"Ghosts II\".",
|
||||
ScheduledStatusID: "",
|
||||
Blurhash: "LZDJO?ayIUof01j[xuayxuayayj[",
|
||||
Processing: 2,
|
||||
File: gtsmodel.File{
|
||||
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01J2M20K6K9XQC4WSB961YJHV6.mp3",
|
||||
ContentType: "audio/mpeg",
|
||||
@@ -1042,7 +1029,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(true),
|
||||
},
|
||||
"local_account_2_status_9_attachment_1": {
|
||||
ID: "01JDQ164HM08SGJ7ZEK9003Z4B",
|
||||
@@ -1051,16 +1037,15 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
RemoteURL: "http://example.org/fileserver/01HE7Y659ZWZ02JM4AWYJZ176Q/attachment/original/01HE892Y8ZS68TQCNPX7J888P3.mp3",
|
||||
CreatedAt: TimeMustParse("2024-11-01T10:01:00+02:00"),
|
||||
Type: gtsmodel.FileTypeUnknown,
|
||||
Error: gtsmodel.NewMediaErrorDetails(gtsmodel.MediaErrorTypeCodec, gtsmodel.MediaErrorTypeCodec_Unsupported),
|
||||
FileMeta: gtsmodel.FileMeta{},
|
||||
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||
Description: "Jolly salsa song, public domain.",
|
||||
Blurhash: "",
|
||||
Processing: gtsmodel.ProcessingStatusProcessed,
|
||||
File: gtsmodel.File{},
|
||||
Thumbnail: gtsmodel.Thumbnail{RemoteURL: ""},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(false),
|
||||
},
|
||||
"local_account_3_avatar": {
|
||||
ID: "01JPHQZ0ZHC2AXJK1JQNXRXQZN",
|
||||
@@ -1091,7 +1076,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
Description: "DESCRIPTION_GOES_HERE",
|
||||
ScheduledStatusID: "",
|
||||
Blurhash: "LRF~2LIU0esp-qRjR*aeJ$s;iwW.",
|
||||
Processing: 2,
|
||||
File: gtsmodel.File{
|
||||
Path: "01JPCMD83Y4WR901094YES3QC5/avatar/original/01JPHQZ0ZHC2AXJK1JQNXRXQZN.jpeg",
|
||||
ContentType: "image/jpeg",
|
||||
@@ -1106,7 +1090,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
},
|
||||
Avatar: util.Ptr(true),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(true),
|
||||
},
|
||||
"local_account_3_header": {
|
||||
ID: "01JPHRB7F2RXPTEQFRYC85EPD9",
|
||||
@@ -1137,7 +1120,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
Description: "DESCRIPTION_GOES_HERE",
|
||||
ScheduledStatusID: "",
|
||||
Blurhash: "L9I5h:%M%M?a~os:D*bFMybFM{jI",
|
||||
Processing: 2,
|
||||
File: gtsmodel.File{
|
||||
Path: "01JPCMD83Y4WR901094YES3QC5/header/original/01JPHRB7F2RXPTEQFRYC85EPD9.png",
|
||||
ContentType: "image/png",
|
||||
@@ -1152,7 +1134,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(true),
|
||||
Cached: util.Ptr(true),
|
||||
},
|
||||
// sickos
|
||||
"local_account_3_status_1_attachment_1": {
|
||||
@@ -1184,7 +1165,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
Description: "DESCRIPTION_GOES_HERE",
|
||||
ScheduledStatusID: "",
|
||||
Blurhash: "L~EqXWX5t6og%jW=owa~N1WFjYWC",
|
||||
Processing: 2,
|
||||
File: gtsmodel.File{
|
||||
Path: "01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPCPRMPPGWKBCAE7X81XA0PK.jpeg",
|
||||
ContentType: "image/jpeg",
|
||||
@@ -1199,7 +1179,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(true),
|
||||
},
|
||||
// marge
|
||||
"local_account_3_status_1_attachment_2": {
|
||||
@@ -1231,7 +1210,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
Description: "DESCRIPTION_GOES_HERE",
|
||||
ScheduledStatusID: "",
|
||||
Blurhash: "LGH1i6RpD;-,0DoZaIogA2N3xZI]",
|
||||
Processing: 2,
|
||||
File: gtsmodel.File{
|
||||
Path: "01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPCPTSFNQDAGTHP49DXSD0BM.png",
|
||||
ContentType: "image/png",
|
||||
@@ -1246,7 +1224,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(true),
|
||||
},
|
||||
// sloth-gear
|
||||
"local_account_3_status_1_attachment_3": {
|
||||
@@ -1278,7 +1255,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
Description: "DESCRIPTION_GOES_HERE",
|
||||
ScheduledStatusID: "",
|
||||
Blurhash: "LOE.|bxZx]j[~pt7WWWW%Lj@%Mj[",
|
||||
Processing: 2,
|
||||
File: gtsmodel.File{
|
||||
Path: "01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPCPYJ6N2E2R7GAJ1XECXNV5.webp",
|
||||
ContentType: "image/webp",
|
||||
@@ -1293,7 +1269,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(true),
|
||||
},
|
||||
// you-posted
|
||||
"local_account_3_status_1_attachment_4": {
|
||||
@@ -1325,7 +1300,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
Description: "DESCRIPTION_GOES_HERE",
|
||||
ScheduledStatusID: "",
|
||||
Blurhash: "L00+zhoLNubHj[fQa|fQ9tWVw{jZ",
|
||||
Processing: 2,
|
||||
File: gtsmodel.File{
|
||||
Path: "01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPCQ4WXEA52VVR9V1HN7E0RS.png",
|
||||
ContentType: "image/png",
|
||||
@@ -1340,7 +1314,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(true),
|
||||
},
|
||||
// buscemi
|
||||
"local_account_3_status_1_attachment_5": {
|
||||
@@ -1372,7 +1345,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
Description: "DESCRIPTION_GOES_HERE",
|
||||
ScheduledStatusID: "",
|
||||
Blurhash: "L5A9A=}?J*5m56Rk={$%O?Nb$M$i",
|
||||
Processing: 2,
|
||||
File: gtsmodel.File{
|
||||
Path: "01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPCQ9VBZBMSTVN56QN3R5188.jpeg",
|
||||
ContentType: "image/jpeg",
|
||||
@@ -1387,7 +1359,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(true),
|
||||
},
|
||||
// butt
|
||||
"local_account_3_status_1_attachment_6": {
|
||||
@@ -1419,7 +1390,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
Description: "DESCRIPTION_GOES_HERE",
|
||||
ScheduledStatusID: "",
|
||||
Blurhash: "LWLN.4~q00ofxuxu-;%M9F-;-;xu",
|
||||
Processing: 2,
|
||||
File: gtsmodel.File{
|
||||
Path: "01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPG1RZPRH3Y00VSA3RQ2SJWP.gif",
|
||||
ContentType: "image/gif",
|
||||
@@ -1434,7 +1404,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(true),
|
||||
},
|
||||
// bunny
|
||||
"local_account_3_status_2_attachment_1": {
|
||||
@@ -1468,7 +1437,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
Description: "DESCRIPTION_GOES_HERE",
|
||||
ScheduledStatusID: "",
|
||||
Blurhash: "LEQcn{?bfQ?b~qoffQoffQfQfQfQ",
|
||||
Processing: 2,
|
||||
File: gtsmodel.File{
|
||||
Path: "01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPHFKQ86GT9W76SWPHE9P8JB.webm",
|
||||
ContentType: "video/webm",
|
||||
@@ -1483,7 +1451,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(true),
|
||||
},
|
||||
// computerbye
|
||||
"local_account_3_status_2_attachment_2": {
|
||||
@@ -1518,7 +1485,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
Description: "DESCRIPTION_GOES_HERE",
|
||||
ScheduledStatusID: "",
|
||||
Blurhash: "LLHUzr-;o#_2~q-:IV%Mxu%MM{M{",
|
||||
Processing: 2,
|
||||
File: gtsmodel.File{
|
||||
Path: "01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPHFSCVGGH02FX9VJMXGXN45.gif",
|
||||
ContentType: "image/gif",
|
||||
@@ -1533,7 +1499,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(true),
|
||||
},
|
||||
// diarrhea
|
||||
"local_account_3_status_2_attachment_3": {
|
||||
@@ -1568,7 +1533,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
Description: "DESCRIPTION_GOES_HERE",
|
||||
ScheduledStatusID: "",
|
||||
Blurhash: "L78qTmNG00xZkWxsIURQ01s;?aR*",
|
||||
Processing: 2,
|
||||
File: gtsmodel.File{
|
||||
Path: "01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPHFW5HKFWQNQ954P5KNXWSR.gif",
|
||||
ContentType: "image/gif",
|
||||
@@ -1583,7 +1547,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(true),
|
||||
},
|
||||
// ffmpreg
|
||||
"local_account_3_status_2_attachment_4": {
|
||||
@@ -1615,7 +1578,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
Description: "DESCRIPTION_GOES_HERE",
|
||||
ScheduledStatusID: "",
|
||||
Blurhash: "LOCX.y}rIpE3,?w{S4W;9vENX8t6",
|
||||
Processing: 2,
|
||||
File: gtsmodel.File{
|
||||
Path: "01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPHFZP2VNS1M2RQ646BXBZQG.jpeg",
|
||||
ContentType: "image/jpeg",
|
||||
@@ -1630,7 +1592,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(true),
|
||||
},
|
||||
// notabug
|
||||
"local_account_3_status_2_attachment_5": {
|
||||
@@ -1662,7 +1623,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
Description: "DESCRIPTION_GOES_HERE",
|
||||
ScheduledStatusID: "",
|
||||
Blurhash: "LTGbrRxAE1og0OR:xve-OFs6kCWY",
|
||||
Processing: 2,
|
||||
File: gtsmodel.File{
|
||||
Path: "01JPCMD83Y4WR901094YES3QC5/attachment/original/01JPHG32F7M6F084WKEGAYJ40X.jpeg",
|
||||
ContentType: "image/jpeg",
|
||||
@@ -1677,7 +1637,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(true),
|
||||
},
|
||||
"remote_account_1_status_1_attachment_1": {
|
||||
ID: "01FVW7RXPQ8YJHTEXYPE7Q8ZY0",
|
||||
@@ -1708,7 +1667,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
Description: "tweet from thoughts of dog: i drank. all the water. in my bowl. earlier. but just now. i returned. to the same bowl. and it was. full again.. the bowl. is haunted",
|
||||
ScheduledStatusID: "",
|
||||
Blurhash: "L3Q9_@4n9E?axW4mD$Mx~q00Di%L",
|
||||
Processing: 2,
|
||||
File: gtsmodel.File{
|
||||
Path: "01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpeg",
|
||||
ContentType: "image/jpeg",
|
||||
@@ -1722,12 +1680,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(true),
|
||||
},
|
||||
"remote_account_3_header": {
|
||||
ID: "01PFPMWK2FF0D9WMHEJHR07C3R",
|
||||
ID: "01G549FP8065NKWBPTWHP6Y3PD",
|
||||
StatusID: "",
|
||||
URL: "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/original/01PFPMWK2FF0D9WMHEJHR07C3R.jpg",
|
||||
URL: "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/original/01G549FP8065NKWBPTWHP6Y3PD.jpg",
|
||||
RemoteURL: "http://fossbros-anonymous.io/attachments/small/a499f55b-2d1e-4acd-98d2-1ac2ba6d79b9.jpg",
|
||||
CreatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
|
||||
Type: gtsmodel.FileTypeImage,
|
||||
@@ -1753,21 +1710,19 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
Description: "tweet from thoughts of dog: i drank. all the water. in my bowl. earlier. but just now. i returned. to the same bowl. and it was. full again.. the bowl. is haunted",
|
||||
ScheduledStatusID: "",
|
||||
Blurhash: "L3Q9_@4n9E?axW4mD$Mx~q00Di%L",
|
||||
Processing: 2,
|
||||
File: gtsmodel.File{
|
||||
Path: "062G5WYKY35KKD12EMSM3F8PJ8/attachment/original/01PFPMWK2FF0D9WMHEJHR07C3R.jpeg",
|
||||
Path: "062G5WYKY35KKD12EMSM3F8PJ8/attachment/original/01G549FP8065NKWBPTWHP6Y3PD.jpeg",
|
||||
ContentType: "image/jpeg",
|
||||
FileSize: 19310,
|
||||
},
|
||||
Thumbnail: gtsmodel.Thumbnail{
|
||||
Path: "062G5WYKY35KKD12EMSM3F8PJ8/attachment/small/01PFPMWK2FF0D9WMHEJHR07C3R.jpeg",
|
||||
Path: "062G5WYKY35KKD12EMSM3F8PJ8/attachment/small/01G549FP8065NKWBPTWHP6Y3PD.jpeg",
|
||||
ContentType: "image/webp",
|
||||
FileSize: 20395,
|
||||
URL: "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/small/01PFPMWK2FF0D9WMHEJHR07C3R.webp",
|
||||
URL: "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/small/01G549FP8065NKWBPTWHP6Y3PD.webp",
|
||||
},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(true),
|
||||
Cached: util.Ptr(true),
|
||||
},
|
||||
"remote_account_2_status_1_attachment_1": {
|
||||
ID: "01HE7Y3C432WRSNS10EZM86SA5",
|
||||
@@ -1797,7 +1752,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
AccountID: "01FHMQX3GAABWSM0S2VZEC2SWC",
|
||||
Description: "Photograph of a sloth, Public Domain.",
|
||||
Blurhash: "LKE3VIw}0KD%a2o{M|t7NFWps:t7",
|
||||
Processing: 2,
|
||||
File: gtsmodel.File{
|
||||
Path: "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE7Y3C432WRSNS10EZM86SA5.jpg",
|
||||
ContentType: "image/jpg",
|
||||
@@ -1811,7 +1765,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(true),
|
||||
},
|
||||
"remote_account_2_status_1_attachment_2": {
|
||||
ID: "01HE7ZFX9GKA5ZZVD4FACABSS9",
|
||||
@@ -1820,16 +1773,15 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
RemoteURL: "http://example.org/fileserver/01HE7Y659ZWZ02JM4AWYJZ176Q/attachment/original/01HE7ZGJYTSYMXF927GF9353KR.svg",
|
||||
CreatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"),
|
||||
Type: gtsmodel.FileTypeUnknown,
|
||||
Error: gtsmodel.NewMediaErrorDetails(gtsmodel.MediaErrorTypeCodec, gtsmodel.MediaErrorTypeCodec_Unsupported),
|
||||
FileMeta: gtsmodel.FileMeta{},
|
||||
AccountID: "01FHMQX3GAABWSM0S2VZEC2SWC",
|
||||
Description: "SVG line art of a sloth, public domain",
|
||||
Blurhash: "L26*j+~qE1RP?wxut7ofRlM{R*of",
|
||||
Processing: 2,
|
||||
File: gtsmodel.File{},
|
||||
Thumbnail: gtsmodel.Thumbnail{RemoteURL: ""},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(false),
|
||||
},
|
||||
"remote_account_2_status_1_attachment_3": {
|
||||
ID: "01HE88YG74PVAB81PX2XA9F3FG",
|
||||
@@ -1838,16 +1790,15 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||
RemoteURL: "http://example.org/fileserver/01HE7Y659ZWZ02JM4AWYJZ176Q/attachment/original/01HE892Y8ZS68TQCNPX7J888P3.mp3",
|
||||
CreatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"),
|
||||
Type: gtsmodel.FileTypeUnknown,
|
||||
Error: gtsmodel.NewMediaErrorDetails(gtsmodel.MediaErrorTypeCodec, gtsmodel.MediaErrorTypeCodec_Unsupported),
|
||||
FileMeta: gtsmodel.FileMeta{},
|
||||
AccountID: "01FHMQX3GAABWSM0S2VZEC2SWC",
|
||||
Description: "Jolly salsa song, public domain.",
|
||||
Blurhash: "",
|
||||
Processing: 2,
|
||||
File: gtsmodel.File{},
|
||||
Thumbnail: gtsmodel.Thumbnail{RemoteURL: ""},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(false),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1875,7 +1826,6 @@ func NewTestEmojis() map[string]*gtsmodel.Emoji {
|
||||
URI: "http://localhost:8080/emoji/01F8MH9H8E4VG3KDYJR9EGPXCQ",
|
||||
VisibleInPicker: util.Ptr(true),
|
||||
CategoryID: "01GGQ8V4993XK67B2JB396YFB7",
|
||||
Cached: util.Ptr(true),
|
||||
},
|
||||
"yell": {
|
||||
ID: "01GD5KP5CQEE1R3X43Y1EHS2CW",
|
||||
@@ -1897,7 +1847,6 @@ func NewTestEmojis() map[string]*gtsmodel.Emoji {
|
||||
URI: "http://fossbros-anonymous.io/emoji/01GD5KP5CQEE1R3X43Y1EHS2CW",
|
||||
VisibleInPicker: util.Ptr(false),
|
||||
CategoryID: "",
|
||||
Cached: util.Ptr(false),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user