[chore] media and emoji refactoring (#3000)
* start updating media manager interface ready for storing attachments / emoji right away * store emoji and media as uncached immediately, then (re-)cache on Processing{}.Load() * remove now unused media workers * fix tests and issues * fix another test! * fix emoji activitypub uri setting behaviour, fix remainder of test compilation issues * fix more tests * fix (most of) remaining tests, add debouncing to repeatedly failing media / emojis * whoops, rebase issue * remove kim's whacky experiments * do some reshuffling, ensure emoji uri gets set * ensure marked as not cached on cleanup * tweaks to media / emoji processing to handle context canceled better * ensure newly fetched emojis actually get set in returned slice * use different varnames to be a bit more obvious * move emoji refresh rate limiting to dereferencer * add exported dereferencer functions for remote media, use these for recaching in processor * add check for nil attachment in updateAttachment() * remove unused emoji and media fields + columns * see previous commit * fix old migrations expecting image_updated_at to exists (from copies of old models) * remove freshness checking code (seems to be broken...) * fix error arg causing nil ptr exception * finish documentating functions with comments, slight tweaks to media / emoji deref error logic * remove some extra unneeded boolean checking * finish writing documentation (code comments) for exported media manager methods * undo changes to migration snapshot gtsmodels, updated failing migration to have its own snapshot * move doesColumnExist() to util.go in migrations package
This commit is contained in:
parent
fa710057c8
commit
21bb324156
|
@ -376,7 +376,17 @@ func (suite *InboxPostTestSuite) TestPostUpdate() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// emojis should be updated
|
// emojis should be updated
|
||||||
suite.Contains(dbUpdatedAccount.EmojiIDs, testEmoji.ID)
|
var haveUpdatedEmoji bool
|
||||||
|
for _, emoji := range dbUpdatedAccount.Emojis {
|
||||||
|
if emoji.Shortcode == testEmoji.Shortcode &&
|
||||||
|
emoji.Domain == testEmoji.Domain &&
|
||||||
|
emoji.ImageRemoteURL == emoji.ImageRemoteURL &&
|
||||||
|
emoji.ImageStaticRemoteURL == emoji.ImageStaticRemoteURL {
|
||||||
|
haveUpdatedEmoji = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
suite.True(haveUpdatedEmoji)
|
||||||
|
|
||||||
// account should be freshly fetched
|
// account should be freshly fetched
|
||||||
suite.WithinDuration(time.Now(), dbUpdatedAccount.FetchedAt, 10*time.Second)
|
suite.WithinDuration(time.Now(), dbUpdatedAccount.FetchedAt, 10*time.Second)
|
||||||
|
|
|
@ -281,7 +281,7 @@ func (suite *EmojiCreateTestSuite) TestEmojiCreateAlreadyExists() {
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotEmpty(b)
|
suite.NotEmpty(b)
|
||||||
|
|
||||||
suite.Equal(`{"error":"Conflict: emoji with shortcode rainbow already exists"}`, string(b))
|
suite.Equal(`{"error":"Conflict: emoji with shortcode already exists"}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEmojiCreateTestSuite(t *testing.T) {
|
func TestEmojiCreateTestSuite(t *testing.T) {
|
||||||
|
|
|
@ -20,6 +20,7 @@ package admin_test
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
@ -370,10 +371,10 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateModifyRemoteEmoji() {
|
||||||
defer result.Body.Close()
|
defer result.Body.Close()
|
||||||
|
|
||||||
// check the response
|
// check the response
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
b, err := io.ReadAll(result.Body)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
suite.Equal(`{"error":"Bad Request: emoji 01GD5KP5CQEE1R3X43Y1EHS2CW is not a local emoji, cannot update it via this endpoint"}`, string(b))
|
suite.Equal(`{"error":"Bad Request: cannot modify remote emoji"}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *EmojiUpdateTestSuite) TestEmojiUpdateModifyNoParams() {
|
func (suite *EmojiUpdateTestSuite) TestEmojiUpdateModifyNoParams() {
|
||||||
|
@ -440,7 +441,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyLocalToLocal() {
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
suite.Equal(`{"error":"Bad Request: emoji 01F8MH9H8E4VG3KDYJR9EGPXCQ is not a remote emoji, cannot copy it to local"}`, string(b))
|
suite.Equal(`{"error":"Bad Request: target emoji is not remote; cannot copy to local"}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyEmptyShortcode() {
|
func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyEmptyShortcode() {
|
||||||
|
@ -541,7 +542,7 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyShortcodeAlreadyInUse() {
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
suite.Equal(`{"error":"Conflict: emoji with shortcode rainbow already exists on this instance"}`, string(b))
|
suite.Equal(`{"error":"Conflict: emoji with shortcode already exists"}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEmojiUpdateTestSuite(t *testing.T) {
|
func TestEmojiUpdateTestSuite(t *testing.T) {
|
||||||
|
|
|
@ -19,7 +19,7 @@ package fileserver_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -28,6 +28,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/fileserver"
|
"github.com/superseriousbusiness/gotosocial/internal/api/fileserver"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/middleware"
|
||||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -54,12 +55,15 @@ func (suite *ServeFileTestSuite) GetFile(
|
||||||
ctx.AddParam(fileserver.MediaSizeKey, string(mediaSize))
|
ctx.AddParam(fileserver.MediaSizeKey, string(mediaSize))
|
||||||
ctx.AddParam(fileserver.FileNameKey, filename)
|
ctx.AddParam(fileserver.FileNameKey, filename)
|
||||||
|
|
||||||
|
logger := middleware.Logger(false)
|
||||||
suite.fileServer.ServeFile(ctx)
|
suite.fileServer.ServeFile(ctx)
|
||||||
|
logger(ctx)
|
||||||
|
|
||||||
code = recorder.Code
|
code = recorder.Code
|
||||||
headers = recorder.Result().Header
|
headers = recorder.Result().Header
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
body, err = ioutil.ReadAll(recorder.Body)
|
body, err = io.ReadAll(recorder.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
suite.FailNow(err.Error())
|
suite.FailNow(err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
@ -334,7 +334,6 @@ func sizeofEmoji() uintptr {
|
||||||
ImageStaticPath: exampleURI,
|
ImageStaticPath: exampleURI,
|
||||||
ImageContentType: "image/png",
|
ImageContentType: "image/png",
|
||||||
ImageStaticContentType: "image/png",
|
ImageStaticContentType: "image/png",
|
||||||
ImageUpdatedAt: exampleTime,
|
|
||||||
Disabled: func() *bool { ok := false; return &ok }(),
|
Disabled: func() *bool { ok := false; return &ok }(),
|
||||||
URI: "http://localhost:8080/emoji/01F8MH9H8E4VG3KDYJR9EGPXCQ",
|
URI: "http://localhost:8080/emoji/01F8MH9H8E4VG3KDYJR9EGPXCQ",
|
||||||
VisibleInPicker: func() *bool { ok := true; return &ok }(),
|
VisibleInPicker: func() *bool { ok := true; return &ok }(),
|
||||||
|
@ -473,12 +472,10 @@ func sizeofMedia() uintptr {
|
||||||
File: gtsmodel.File{
|
File: gtsmodel.File{
|
||||||
Path: exampleURI,
|
Path: exampleURI,
|
||||||
ContentType: "image/jpeg",
|
ContentType: "image/jpeg",
|
||||||
UpdatedAt: exampleTime,
|
|
||||||
},
|
},
|
||||||
Thumbnail: gtsmodel.Thumbnail{
|
Thumbnail: gtsmodel.Thumbnail{
|
||||||
Path: exampleURI,
|
Path: exampleURI,
|
||||||
ContentType: "image/jpeg",
|
ContentType: "image/jpeg",
|
||||||
UpdatedAt: exampleTime,
|
|
||||||
URL: exampleURI,
|
URL: exampleURI,
|
||||||
RemoteURL: exampleURI,
|
RemoteURL: exampleURI,
|
||||||
},
|
},
|
||||||
|
|
|
@ -386,11 +386,10 @@ func (suite *MediaTestSuite) TestUncacheAndRecache() {
|
||||||
testStatusAttachment,
|
testStatusAttachment,
|
||||||
testHeader,
|
testHeader,
|
||||||
} {
|
} {
|
||||||
processingRecache, err := suite.manager.PreProcessMediaRecache(ctx, data, original.ID)
|
processing := suite.manager.RecacheMedia(original, data)
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
// synchronously load the recached attachment
|
// synchronously load the recached attachment
|
||||||
recachedAttachment, err := processingRecache.LoadAttachment(ctx)
|
recachedAttachment, err := processing.Load(ctx)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(recachedAttachment)
|
suite.NotNil(recachedAttachment)
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ package migrations
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20230521105850_emoji_empty_domain_fix"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
"github.com/uptrace/bun/dialect"
|
"github.com/uptrace/bun/dialect"
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
// GoToSocial
|
||||||
|
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package gtsmodel
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Emoji represents a custom emoji that's been uploaded through the admin UI or downloaded from a remote instance.
|
||||||
|
type Emoji struct {
|
||||||
|
ID string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||||
|
CreatedAt time.Time `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||||
|
UpdatedAt time.Time `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||||
|
Shortcode string `validate:"required" bun:",nullzero,notnull,unique:domainshortcode"` // String shortcode for this emoji -- the part that's between colons. This should be lowercase a-z_ eg., 'blob_hug' 'purple_heart' Must be unique with domain.
|
||||||
|
Domain string `validate:"omitempty,fqdn" bun:",nullzero,unique:domainshortcode"` // Origin domain of this emoji, eg 'example.org', 'queer.party'. empty string for local emojis.
|
||||||
|
ImageRemoteURL string `validate:"required_without=ImageURL,omitempty,url" bun:",nullzero"` // Where can this emoji be retrieved remotely? Null for local emojis.
|
||||||
|
ImageStaticRemoteURL string `validate:"required_without=ImageStaticURL,omitempty,url" bun:",nullzero"` // Where can a static / non-animated version of this emoji be retrieved remotely? Null for local emojis.
|
||||||
|
ImageURL string `validate:"required_without=ImageRemoteURL,required_without=Domain,omitempty,url" bun:",nullzero"` // Where can this emoji be retrieved from the local server? Null for remote emojis.
|
||||||
|
ImageStaticURL string `validate:"required_without=ImageStaticRemoteURL,required_without=Domain,omitempty,url" bun:",nullzero"` // Where can a static version of this emoji be retrieved from the local server? Null for remote emojis.
|
||||||
|
ImagePath string `validate:"required,file" bun:",nullzero,notnull"` // Path of the emoji image in the server storage system.
|
||||||
|
ImageStaticPath string `validate:"required,file" bun:",nullzero,notnull"` // Path of a static version of the emoji image in the server storage system
|
||||||
|
ImageContentType string `validate:"required" bun:",nullzero,notnull"` // MIME content type of the emoji image
|
||||||
|
ImageStaticContentType string `validate:"required" bun:",nullzero,notnull"` // MIME content type of the static version of the emoji image.
|
||||||
|
ImageFileSize int `validate:"required,min=1" bun:",nullzero,notnull"` // Size of the emoji image file in bytes, for serving purposes.
|
||||||
|
ImageStaticFileSize int `validate:"required,min=1" bun:",nullzero,notnull"` // Size of the static version of the emoji image file in bytes, for serving purposes.
|
||||||
|
ImageUpdatedAt time.Time `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // When was the emoji image last updated?
|
||||||
|
Disabled *bool `validate:"-" bun:",nullzero,notnull,default:false"` // Has a moderation action disabled this emoji from being shown?
|
||||||
|
URI string `validate:"url" bun:",nullzero,notnull,unique"` // ActivityPub uri of this emoji. Something like 'https://example.org/emojis/1234'
|
||||||
|
VisibleInPicker *bool `validate:"-" bun:",nullzero,notnull,default:true"` // Is this emoji visible in the admin emoji picker?
|
||||||
|
Category *EmojiCategory `validate:"-" bun:"rel:belongs-to"` // In which emoji category is this emoji visible?
|
||||||
|
CategoryID string `validate:"omitempty,ulid" bun:"type:CHAR(26),nullzero"` // ID of the category this emoji belongs to.
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmojiCategory represents a grouping of custom emojis.
|
||||||
|
type EmojiCategory struct {
|
||||||
|
ID string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||||
|
CreatedAt time.Time `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||||
|
UpdatedAt time.Time `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||||
|
Name string `validate:"required" bun:",nullzero,notnull,unique"` // name of this category
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
// GoToSocial
|
||||||
|
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
up := func(ctx context.Context, db *bun.DB) error {
|
||||||
|
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||||
|
|
||||||
|
for _, dropcase := range []struct {
|
||||||
|
table string
|
||||||
|
col string
|
||||||
|
}{
|
||||||
|
{table: "media_attachments", col: "file_updated_at"},
|
||||||
|
{table: "media_attachments", col: "thumbnail_updated_at"},
|
||||||
|
{table: "emojis", col: "thumbnail_updated_at"},
|
||||||
|
} {
|
||||||
|
// For each case check the column actually exists on database.
|
||||||
|
exists, err := doesColumnExist(ctx, tx, dropcase.table, dropcase.col)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
// Now actually drop the column.
|
||||||
|
if _, err := tx.NewDropColumn().
|
||||||
|
Table(dropcase.table).
|
||||||
|
Column(dropcase.col).
|
||||||
|
Exec(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
down := func(ctx context.Context, db *bun.DB) error {
|
||||||
|
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := Migrations.Register(up, down); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
// GoToSocial
|
||||||
|
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
"github.com/uptrace/bun/dialect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// doesColumnExist safely checks whether given column exists on table, handling both SQLite and PostgreSQL appropriately.
|
||||||
|
func doesColumnExist(ctx context.Context, tx bun.Tx, table, col string) (bool, error) {
|
||||||
|
var n int
|
||||||
|
var err error
|
||||||
|
switch tx.Dialect().Name() {
|
||||||
|
case dialect.SQLite:
|
||||||
|
err = tx.NewRaw("SELECT COUNT(*) FROM pragma_table_info(?) WHERE name=?", table, col).Scan(ctx, &n)
|
||||||
|
case dialect.PG:
|
||||||
|
err = tx.NewRaw("SELECT COUNT(*) FROM information_schema.columns WHERE table_name=? and column_name=?", table, col).Scan(ctx, &n)
|
||||||
|
default:
|
||||||
|
panic("unexpected dialect")
|
||||||
|
}
|
||||||
|
return (n > 0), err
|
||||||
|
}
|
|
@ -33,7 +33,6 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -730,18 +729,18 @@ func (d *Dereferencer) enrichAccount(
|
||||||
latestAcc.ID = account.ID
|
latestAcc.ID = account.ID
|
||||||
latestAcc.FetchedAt = time.Now()
|
latestAcc.FetchedAt = time.Now()
|
||||||
|
|
||||||
// Ensure the account's avatar media is populated, passing in existing to check for changes.
|
// Ensure the account's avatar media is populated, passing in existing to check for chages.
|
||||||
if err := d.fetchRemoteAccountAvatar(ctx, tsport, account, latestAcc); err != nil {
|
if err := d.fetchAccountAvatar(ctx, requestUser, account, latestAcc); err != nil {
|
||||||
log.Errorf(ctx, "error fetching remote avatar for account %s: %v", uri, err)
|
log.Errorf(ctx, "error fetching remote avatar for account %s: %v", uri, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the account's avatar media is populated, passing in existing to check for changes.
|
// Ensure the account's avatar media is populated, passing in existing to check for chages.
|
||||||
if err := d.fetchRemoteAccountHeader(ctx, tsport, account, latestAcc); err != nil {
|
if err := d.fetchAccountHeader(ctx, requestUser, account, latestAcc); err != nil {
|
||||||
log.Errorf(ctx, "error fetching remote header for account %s: %v", uri, err)
|
log.Errorf(ctx, "error fetching remote header for account %s: %v", uri, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the latest remote account emoji IDs used in account display name/bio.
|
// Fetch the latest remote account emoji IDs used in account display name/bio.
|
||||||
if _, err = d.fetchRemoteAccountEmojis(ctx, latestAcc, requestUser); err != nil {
|
if err = d.fetchAccountEmojis(ctx, account, latestAcc); err != nil {
|
||||||
log.Errorf(ctx, "error fetching remote emojis for account %s: %v", uri, err)
|
log.Errorf(ctx, "error fetching remote emojis for account %s: %v", uri, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -779,9 +778,9 @@ func (d *Dereferencer) enrichAccount(
|
||||||
return latestAcc, apubAcc, nil
|
return latestAcc, apubAcc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Dereferencer) fetchRemoteAccountAvatar(
|
func (d *Dereferencer) fetchAccountAvatar(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
tsport transport.Transport,
|
requestUser string,
|
||||||
existingAcc *gtsmodel.Account,
|
existingAcc *gtsmodel.Account,
|
||||||
latestAcc *gtsmodel.Account,
|
latestAcc *gtsmodel.Account,
|
||||||
) error {
|
) error {
|
||||||
|
@ -808,7 +807,7 @@ func (d *Dereferencer) fetchRemoteAccountAvatar(
|
||||||
// Ensuring existing attachment is up-to-date
|
// Ensuring existing attachment is up-to-date
|
||||||
// and any recaching is performed if required.
|
// and any recaching is performed if required.
|
||||||
existing, err := d.updateAttachment(ctx,
|
existing, err := d.updateAttachment(ctx,
|
||||||
tsport,
|
requestUser,
|
||||||
existing,
|
existing,
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
|
@ -830,20 +829,25 @@ func (d *Dereferencer) fetchRemoteAccountAvatar(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch newly changed avatar from remote.
|
// Fetch newly changed avatar.
|
||||||
attachment, err := d.loadAttachment(ctx,
|
attachment, err := d.GetMedia(ctx,
|
||||||
tsport,
|
requestUser,
|
||||||
latestAcc.ID,
|
latestAcc.ID,
|
||||||
latestAcc.AvatarRemoteURL,
|
latestAcc.AvatarRemoteURL,
|
||||||
&media.AdditionalMediaInfo{
|
media.AdditionalMediaInfo{
|
||||||
Avatar: util.Ptr(true),
|
Avatar: util.Ptr(true),
|
||||||
RemoteURL: &latestAcc.AvatarRemoteURL,
|
RemoteURL: &latestAcc.AvatarRemoteURL,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if attachment == nil {
|
||||||
return gtserror.Newf("error loading attachment %s: %w", latestAcc.AvatarRemoteURL, err)
|
return gtserror.Newf("error loading attachment %s: %w", latestAcc.AvatarRemoteURL, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// non-fatal error occurred during loading, still use it.
|
||||||
|
log.Warnf(ctx, "partially loaded attachment: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Set the avatar attachment on account model.
|
// Set the avatar attachment on account model.
|
||||||
latestAcc.AvatarMediaAttachment = attachment
|
latestAcc.AvatarMediaAttachment = attachment
|
||||||
latestAcc.AvatarMediaAttachmentID = attachment.ID
|
latestAcc.AvatarMediaAttachmentID = attachment.ID
|
||||||
|
@ -851,9 +855,9 @@ func (d *Dereferencer) fetchRemoteAccountAvatar(
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Dereferencer) fetchRemoteAccountHeader(
|
func (d *Dereferencer) fetchAccountHeader(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
tsport transport.Transport,
|
requestUser string,
|
||||||
existingAcc *gtsmodel.Account,
|
existingAcc *gtsmodel.Account,
|
||||||
latestAcc *gtsmodel.Account,
|
latestAcc *gtsmodel.Account,
|
||||||
) error {
|
) error {
|
||||||
|
@ -880,7 +884,7 @@ func (d *Dereferencer) fetchRemoteAccountHeader(
|
||||||
// Ensuring existing attachment is up-to-date
|
// Ensuring existing attachment is up-to-date
|
||||||
// and any recaching is performed if required.
|
// and any recaching is performed if required.
|
||||||
existing, err := d.updateAttachment(ctx,
|
existing, err := d.updateAttachment(ctx,
|
||||||
tsport,
|
requestUser,
|
||||||
existing,
|
existing,
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
|
@ -902,20 +906,25 @@ func (d *Dereferencer) fetchRemoteAccountHeader(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch newly changed header from remote.
|
// Fetch newly changed header.
|
||||||
attachment, err := d.loadAttachment(ctx,
|
attachment, err := d.GetMedia(ctx,
|
||||||
tsport,
|
requestUser,
|
||||||
latestAcc.ID,
|
latestAcc.ID,
|
||||||
latestAcc.HeaderRemoteURL,
|
latestAcc.HeaderRemoteURL,
|
||||||
&media.AdditionalMediaInfo{
|
media.AdditionalMediaInfo{
|
||||||
Header: util.Ptr(true),
|
Header: util.Ptr(true),
|
||||||
RemoteURL: &latestAcc.HeaderRemoteURL,
|
RemoteURL: &latestAcc.HeaderRemoteURL,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if attachment == nil {
|
||||||
return gtserror.Newf("error loading attachment %s: %w", latestAcc.HeaderRemoteURL, err)
|
return gtserror.Newf("error loading attachment %s: %w", latestAcc.HeaderRemoteURL, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// non-fatal error occurred during loading, still use it.
|
||||||
|
log.Warnf(ctx, "partially loaded attachment: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Set the header attachment on account model.
|
// Set the header attachment on account model.
|
||||||
latestAcc.HeaderMediaAttachment = attachment
|
latestAcc.HeaderMediaAttachment = attachment
|
||||||
latestAcc.HeaderMediaAttachmentID = attachment.ID
|
latestAcc.HeaderMediaAttachmentID = attachment.ID
|
||||||
|
@ -923,119 +932,44 @@ func (d *Dereferencer) fetchRemoteAccountHeader(
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Dereferencer) fetchRemoteAccountEmojis(ctx context.Context, targetAccount *gtsmodel.Account, requestingUsername string) (bool, error) {
|
func (d *Dereferencer) fetchAccountEmojis(
|
||||||
maybeEmojis := targetAccount.Emojis
|
ctx context.Context,
|
||||||
maybeEmojiIDs := targetAccount.EmojiIDs
|
existing *gtsmodel.Account,
|
||||||
|
account *gtsmodel.Account,
|
||||||
// It's possible that the account had emoji IDs set on it, but not Emojis
|
) error {
|
||||||
// themselves, depending on how it was fetched before being passed to us.
|
// Fetch the updated emojis for our account.
|
||||||
//
|
emojis, changed, err := d.fetchEmojis(ctx,
|
||||||
// If we only have IDs, fetch the emojis from the db. We know they're in
|
existing.Emojis,
|
||||||
// there or else they wouldn't have IDs.
|
account.Emojis,
|
||||||
if len(maybeEmojiIDs) > len(maybeEmojis) {
|
|
||||||
maybeEmojis = make([]*gtsmodel.Emoji, 0, len(maybeEmojiIDs))
|
|
||||||
for _, emojiID := range maybeEmojiIDs {
|
|
||||||
maybeEmoji, err := d.state.DB.GetEmojiByID(ctx, emojiID)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
maybeEmojis = append(maybeEmojis, maybeEmoji)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// For all the maybe emojis we have, we either fetch them from the database
|
|
||||||
// (if we haven't already), or dereference them from the remote instance.
|
|
||||||
gotEmojis, err := d.populateEmojis(ctx, maybeEmojis, requestingUsername)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the ID of each fetched or dereferenced emoji, so we can attach
|
|
||||||
// this to the account if necessary.
|
|
||||||
gotEmojiIDs := make([]string, 0, len(gotEmojis))
|
|
||||||
for _, e := range gotEmojis {
|
|
||||||
gotEmojiIDs = append(gotEmojiIDs, e.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
changed = false // have the emojis for this account changed?
|
|
||||||
maybeLen = len(maybeEmojis)
|
|
||||||
gotLen = len(gotEmojis)
|
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
// if the length of everything is zero, this is simple:
|
return gtserror.Newf("error fetching emojis: %w", err)
|
||||||
// nothing has changed and there's nothing to do
|
|
||||||
if maybeLen == 0 && gotLen == 0 {
|
|
||||||
return changed, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the *amount* of emojis on the account has changed, then the got emojis
|
if !changed {
|
||||||
// are definitely different from the previous ones (if there were any) --
|
// Use existing account emoji objects.
|
||||||
// the account has either more or fewer emojis set on it now, so take the
|
account.EmojiIDs = existing.EmojiIDs
|
||||||
// discovered emojis as the new correct ones.
|
account.Emojis = existing.Emojis
|
||||||
if maybeLen != gotLen {
|
return nil
|
||||||
changed = true
|
|
||||||
targetAccount.Emojis = gotEmojis
|
|
||||||
targetAccount.EmojiIDs = gotEmojiIDs
|
|
||||||
return changed, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the lengths are the same but not all of the slices are
|
// Set latest emojis.
|
||||||
// zero, something *might* have changed, so we have to check
|
account.Emojis = emojis
|
||||||
|
|
||||||
// 1. did we have emojis before that we don't have now?
|
// Iterate over and set changed emoji IDs.
|
||||||
for _, maybeEmoji := range maybeEmojis {
|
account.EmojiIDs = make([]string, len(emojis))
|
||||||
var stillPresent bool
|
for i, emoji := range emojis {
|
||||||
|
account.EmojiIDs[i] = emoji.ID
|
||||||
for _, gotEmoji := range gotEmojis {
|
|
||||||
if maybeEmoji.URI == gotEmoji.URI {
|
|
||||||
// the emoji we maybe had is still present now,
|
|
||||||
// so we can stop checking gotEmojis
|
|
||||||
stillPresent = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !stillPresent {
|
return nil
|
||||||
// at least one maybeEmoji is no longer present in
|
|
||||||
// the got emojis, so we can stop checking now
|
|
||||||
changed = true
|
|
||||||
targetAccount.Emojis = gotEmojis
|
|
||||||
targetAccount.EmojiIDs = gotEmojiIDs
|
|
||||||
return changed, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. do we have emojis now that we didn't have before?
|
func (d *Dereferencer) dereferenceAccountStats(
|
||||||
for _, gotEmoji := range gotEmojis {
|
ctx context.Context,
|
||||||
var wasPresent bool
|
requestUser string,
|
||||||
|
account *gtsmodel.Account,
|
||||||
for _, maybeEmoji := range maybeEmojis {
|
) error {
|
||||||
// check emoji IDs here as well, because unreferenced
|
|
||||||
// maybe emojis we didn't already have would not have
|
|
||||||
// had IDs set on them yet
|
|
||||||
if gotEmoji.URI == maybeEmoji.URI && gotEmoji.ID == maybeEmoji.ID {
|
|
||||||
// this got emoji was present already in the maybeEmoji,
|
|
||||||
// so we can stop checking through maybeEmojis
|
|
||||||
wasPresent = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !wasPresent {
|
|
||||||
// at least one gotEmojis was not present in
|
|
||||||
// the maybeEmojis, so we can stop checking now
|
|
||||||
changed = true
|
|
||||||
targetAccount.Emojis = gotEmojis
|
|
||||||
targetAccount.EmojiIDs = gotEmojiIDs
|
|
||||||
return changed, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return changed, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Dereferencer) dereferenceAccountStats(ctx context.Context, requestUser string, account *gtsmodel.Account) error {
|
|
||||||
// Ensure we have a stats model for this account.
|
// Ensure we have a stats model for this account.
|
||||||
if account.Stats == nil {
|
if account.Stats == nil {
|
||||||
if err := d.state.DB.PopulateAccountStats(ctx, account); err != nil {
|
if err := d.state.DB.PopulateAccountStats(ctx, account); err != nil {
|
||||||
|
|
|
@ -19,29 +19,190 @@ package dereferencing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d *Dereferencer) GetRemoteEmoji(ctx context.Context, requestUser string, remoteURL string, shortcode string, domain string, id string, emojiURI string, ai *media.AdditionalEmojiInfo, refresh bool) (*media.ProcessingEmoji, error) {
|
// GetEmoji fetches the emoji with given shortcode,
|
||||||
var shortcodeDomain = shortcode + "@" + domain
|
// domain and remote URL to dereference it by. This
|
||||||
|
// handles the case of existing emojis by passing them
|
||||||
// Ensure we have been passed a valid URL.
|
// to RefreshEmoji(), which in the case of a local
|
||||||
derefURI, err := url.Parse(remoteURL)
|
// emoji will be a no-op. If the emoji does not yet
|
||||||
if err != nil {
|
// exist it will be newly inserted into the database
|
||||||
return nil, fmt.Errorf("GetRemoteEmoji: error parsing url for emoji %s: %s", shortcodeDomain, err)
|
// followed by dereferencing the actual media file.
|
||||||
|
//
|
||||||
|
// Please note that even if an error is returned,
|
||||||
|
// an emoji model may still be returned if the error
|
||||||
|
// was only encountered during actual dereferencing.
|
||||||
|
// In this case, it will act as a placeholder.
|
||||||
|
func (d *Dereferencer) GetEmoji(
|
||||||
|
ctx context.Context,
|
||||||
|
shortcode string,
|
||||||
|
domain string,
|
||||||
|
remoteURL string,
|
||||||
|
info media.AdditionalEmojiInfo,
|
||||||
|
refresh bool,
|
||||||
|
) (
|
||||||
|
*gtsmodel.Emoji,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
// Look for an existing emoji with shortcode domain.
|
||||||
|
emoji, err := d.state.DB.GetEmojiByShortcodeDomain(ctx,
|
||||||
|
shortcode,
|
||||||
|
domain,
|
||||||
|
)
|
||||||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
return nil, gtserror.Newf("error fetching emoji from db: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Acquire derefs lock.
|
if emoji != nil {
|
||||||
|
// This was an existing emoji, pass to refresh func.
|
||||||
|
return d.RefreshEmoji(ctx, emoji, info, refresh)
|
||||||
|
}
|
||||||
|
|
||||||
|
if domain == "" {
|
||||||
|
// failed local lookup, will be db.ErrNoEntries.
|
||||||
|
return nil, gtserror.SetUnretrievable(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate shortcode domain for locks + logging.
|
||||||
|
shortcodeDomain := shortcode + "@" + domain
|
||||||
|
|
||||||
|
// Ensure we have a valid remote URL.
|
||||||
|
url, err := url.Parse(remoteURL)
|
||||||
|
if err != nil {
|
||||||
|
err := gtserror.Newf("invalid image remote url %s for emoji %s: %w", remoteURL, shortcodeDomain, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Acquire new instance account transport for emoji dereferencing.
|
||||||
|
tsport, err := d.transportController.NewTransportForUsername(ctx, "")
|
||||||
|
if err != nil {
|
||||||
|
err := gtserror.Newf("error getting instance transport: %w", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare data function to dereference remote emoji media.
|
||||||
|
data := func(context.Context) (io.ReadCloser, int64, error) {
|
||||||
|
return tsport.DereferenceMedia(ctx, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass along for safe processing.
|
||||||
|
return d.processEmojiSafely(ctx,
|
||||||
|
shortcodeDomain,
|
||||||
|
func() (*media.ProcessingEmoji, error) {
|
||||||
|
return d.mediaManager.CreateEmoji(ctx,
|
||||||
|
shortcode,
|
||||||
|
domain,
|
||||||
|
data,
|
||||||
|
info,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshEmoji ensures that the given emoji is
|
||||||
|
// up-to-date, both in terms of being cached in
|
||||||
|
// in local instance storage, and compared to extra
|
||||||
|
// information provided in media.AdditionEmojiInfo{}.
|
||||||
|
// (note that is a no-op to pass in a local emoji).
|
||||||
|
//
|
||||||
|
// Please note that even if an error is returned,
|
||||||
|
// an emoji model may still be returned if the error
|
||||||
|
// was only encountered during actual dereferencing.
|
||||||
|
// In this case, it will act as a placeholder.
|
||||||
|
func (d *Dereferencer) RefreshEmoji(
|
||||||
|
ctx context.Context,
|
||||||
|
emoji *gtsmodel.Emoji,
|
||||||
|
info media.AdditionalEmojiInfo,
|
||||||
|
force bool,
|
||||||
|
) (
|
||||||
|
*gtsmodel.Emoji,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
// Can't refresh local.
|
||||||
|
if emoji.IsLocal() {
|
||||||
|
return emoji, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check emoji is up-to-date
|
||||||
|
// with provided extra info.
|
||||||
|
switch {
|
||||||
|
case info.URI != nil &&
|
||||||
|
*info.URI != emoji.URI:
|
||||||
|
force = true
|
||||||
|
case info.ImageRemoteURL != nil &&
|
||||||
|
*info.ImageRemoteURL != emoji.ImageRemoteURL:
|
||||||
|
force = true
|
||||||
|
case info.ImageStaticRemoteURL != nil &&
|
||||||
|
*info.ImageStaticRemoteURL != emoji.ImageStaticRemoteURL:
|
||||||
|
force = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if needs updating.
|
||||||
|
if !force && *emoji.Cached {
|
||||||
|
return emoji, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: more finegrained freshness checks.
|
||||||
|
|
||||||
|
// Generate shortcode domain for locks + logging.
|
||||||
|
shortcodeDomain := emoji.Shortcode + "@" + emoji.Domain
|
||||||
|
|
||||||
|
// Ensure we have a valid image remote URL.
|
||||||
|
url, err := url.Parse(emoji.ImageRemoteURL)
|
||||||
|
if err != nil {
|
||||||
|
err := gtserror.Newf("invalid image remote url %s for emoji %s: %w", emoji.ImageRemoteURL, shortcodeDomain, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Acquire new instance account transport for emoji dereferencing.
|
||||||
|
tsport, err := d.transportController.NewTransportForUsername(ctx, "")
|
||||||
|
if err != nil {
|
||||||
|
err := gtserror.Newf("error getting instance transport: %w", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare data function to dereference remote emoji media.
|
||||||
|
data := func(context.Context) (io.ReadCloser, int64, error) {
|
||||||
|
return tsport.DereferenceMedia(ctx, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass along for safe processing.
|
||||||
|
return d.processEmojiSafely(ctx,
|
||||||
|
shortcodeDomain,
|
||||||
|
func() (*media.ProcessingEmoji, error) {
|
||||||
|
return d.mediaManager.RefreshEmoji(ctx,
|
||||||
|
emoji,
|
||||||
|
data,
|
||||||
|
info,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// processingEmojiSafely provides concurrency-safe processing of
|
||||||
|
// an emoji with given shortcode+domain. if a copy of the emoji is
|
||||||
|
// not already being processed, the given 'process' callback will
|
||||||
|
// be used to generate new *media.ProcessingEmoji{} instance.
|
||||||
|
func (d *Dereferencer) processEmojiSafely(
|
||||||
|
ctx context.Context,
|
||||||
|
shortcodeDomain string,
|
||||||
|
process func() (*media.ProcessingEmoji, error),
|
||||||
|
) (
|
||||||
|
emoji *gtsmodel.Emoji,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
|
||||||
|
// Acquire map lock.
|
||||||
d.derefEmojisMu.Lock()
|
d.derefEmojisMu.Lock()
|
||||||
|
|
||||||
// Ensure unlock only done once.
|
// Ensure unlock only done once.
|
||||||
|
@ -53,146 +214,118 @@ func (d *Dereferencer) GetRemoteEmoji(ctx context.Context, requestUser string, r
|
||||||
processing, ok := d.derefEmojis[shortcodeDomain]
|
processing, ok := d.derefEmojis[shortcodeDomain]
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
// Fetch a transport for current request user in order to perform request.
|
// Start new processing emoji.
|
||||||
tsport, err := d.transportController.NewTransportForUsername(ctx, requestUser)
|
processing, err = process()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.Newf("couldn't create transport: %w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the media data function to dereference emoji from URI.
|
|
||||||
data := func(ctx context.Context) (io.ReadCloser, int64, error) {
|
|
||||||
return tsport.DereferenceMedia(ctx, derefURI)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new emoji processing request from the media manager.
|
|
||||||
processing, err = d.mediaManager.PreProcessEmoji(ctx, data,
|
|
||||||
shortcode,
|
|
||||||
id,
|
|
||||||
emojiURI,
|
|
||||||
ai,
|
|
||||||
refresh,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.Newf("error preprocessing emoji %s: %s", shortcodeDomain, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store media in map to mark as processing.
|
|
||||||
d.derefEmojis[shortcodeDomain] = processing
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
// On exit safely remove emoji from map.
|
|
||||||
d.derefEmojisMu.Lock()
|
|
||||||
delete(d.derefEmojis, shortcodeDomain)
|
|
||||||
d.derefEmojisMu.Unlock()
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unlock map.
|
// Unlock map.
|
||||||
unlock()
|
unlock()
|
||||||
|
|
||||||
// Start emoji attachment loading (blocking call).
|
// Perform emoji load operation.
|
||||||
if _, err := processing.LoadEmoji(ctx); err != nil {
|
emoji, err = processing.Load(ctx)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return processing, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Dereferencer) populateEmojis(ctx context.Context, rawEmojis []*gtsmodel.Emoji, requestingUsername string) ([]*gtsmodel.Emoji, error) {
|
|
||||||
// At this point we should know:
|
|
||||||
// * the AP uri of the emoji
|
|
||||||
// * the domain of the emoji
|
|
||||||
// * the shortcode of the emoji
|
|
||||||
// * the remote URL of the image
|
|
||||||
// This should be enough to dereference the emoji
|
|
||||||
gotEmojis := make([]*gtsmodel.Emoji, 0, len(rawEmojis))
|
|
||||||
|
|
||||||
for _, e := range rawEmojis {
|
|
||||||
var gotEmoji *gtsmodel.Emoji
|
|
||||||
var err error
|
|
||||||
shortcodeDomain := e.Shortcode + "@" + e.Domain
|
|
||||||
|
|
||||||
// check if we already know this emoji
|
|
||||||
if e.ID != "" {
|
|
||||||
// we had an ID for this emoji already, which means
|
|
||||||
// it should be fleshed out already and we won't
|
|
||||||
// have to get it from the database again
|
|
||||||
gotEmoji = e
|
|
||||||
} else if gotEmoji, err = d.state.DB.GetEmojiByShortcodeDomain(ctx, e.Shortcode, e.Domain); err != nil && err != db.ErrNoEntries {
|
|
||||||
log.Errorf(ctx, "error checking database for emoji %s: %s", shortcodeDomain, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var refresh bool
|
|
||||||
|
|
||||||
if gotEmoji != nil {
|
|
||||||
// we had the emoji already, but refresh it if necessary
|
|
||||||
if e.UpdatedAt.Unix() > gotEmoji.ImageUpdatedAt.Unix() {
|
|
||||||
log.Tracef(ctx, "emoji %s was updated since we last saw it, will refresh", shortcodeDomain)
|
|
||||||
refresh = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !refresh && (e.URI != gotEmoji.URI) {
|
|
||||||
log.Tracef(ctx, "emoji %s changed URI since we last saw it, will refresh", shortcodeDomain)
|
|
||||||
refresh = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !refresh && (e.ImageRemoteURL != gotEmoji.ImageRemoteURL) {
|
|
||||||
log.Tracef(ctx, "emoji %s changed image URL since we last saw it, will refresh", shortcodeDomain)
|
|
||||||
refresh = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !refresh {
|
|
||||||
log.Tracef(ctx, "emoji %s is up to date, will not refresh", shortcodeDomain)
|
|
||||||
} else {
|
|
||||||
log.Tracef(ctx, "refreshing emoji %s", shortcodeDomain)
|
|
||||||
emojiID := gotEmoji.ID // use existing ID
|
|
||||||
processingEmoji, err := d.GetRemoteEmoji(ctx, requestingUsername, e.ImageRemoteURL, e.Shortcode, e.Domain, emojiID, e.URI, &media.AdditionalEmojiInfo{
|
|
||||||
Domain: &e.Domain,
|
|
||||||
ImageRemoteURL: &e.ImageRemoteURL,
|
|
||||||
ImageStaticRemoteURL: &e.ImageStaticRemoteURL,
|
|
||||||
Disabled: gotEmoji.Disabled,
|
|
||||||
VisibleInPicker: gotEmoji.VisibleInPicker,
|
|
||||||
}, refresh)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(ctx, "couldn't refresh remote emoji %s: %s", shortcodeDomain, err)
|
err = gtserror.Newf("error loading emoji %s: %w", shortcodeDomain, err)
|
||||||
continue
|
|
||||||
|
// TODO: in time we should return checkable flags by gtserror.Is___()
|
||||||
|
// which can determine if loading error should allow remaining placeholder.
|
||||||
}
|
}
|
||||||
|
|
||||||
if gotEmoji, err = processingEmoji.LoadEmoji(ctx); err != nil {
|
// Return a COPY of emoji.
|
||||||
log.Errorf(ctx, "couldn't load refreshed remote emoji %s: %s", shortcodeDomain, err)
|
emoji2 := new(gtsmodel.Emoji)
|
||||||
continue
|
*emoji2 = *emoji
|
||||||
|
return emoji2, err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
func (d *Dereferencer) fetchEmojis(
|
||||||
// it's new! go get it!
|
ctx context.Context,
|
||||||
newEmojiID, err := id.NewRandomULID()
|
existing []*gtsmodel.Emoji,
|
||||||
|
emojis []*gtsmodel.Emoji, // newly dereferenced
|
||||||
|
) (
|
||||||
|
[]*gtsmodel.Emoji,
|
||||||
|
bool, // any changes?
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
// Track any changes.
|
||||||
|
changed := false
|
||||||
|
|
||||||
|
for i, placeholder := range emojis {
|
||||||
|
// Look for an existing emoji with shortcode + domain.
|
||||||
|
existing, ok := getEmojiByShortcodeDomain(existing,
|
||||||
|
placeholder.Shortcode,
|
||||||
|
placeholder.Domain,
|
||||||
|
)
|
||||||
|
if ok && existing.ID != "" {
|
||||||
|
|
||||||
|
// Check for any emoji changes that
|
||||||
|
// indicate we should force a refresh.
|
||||||
|
force := emojiChanged(existing, placeholder)
|
||||||
|
|
||||||
|
// Ensure that the existing emoji model is up-to-date and cached.
|
||||||
|
existing, err := d.RefreshEmoji(ctx, existing, media.AdditionalEmojiInfo{
|
||||||
|
|
||||||
|
// Set latest values from placeholder.
|
||||||
|
URI: &placeholder.URI,
|
||||||
|
ImageRemoteURL: &placeholder.ImageRemoteURL,
|
||||||
|
ImageStaticRemoteURL: &placeholder.ImageStaticRemoteURL,
|
||||||
|
}, force)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(ctx, "error generating id for remote emoji %s: %s", shortcodeDomain, err)
|
log.Errorf(ctx, "error refreshing emoji: %v", err)
|
||||||
|
|
||||||
|
// specifically do NOT continue here,
|
||||||
|
// we already have a model, we don't
|
||||||
|
// want to drop it from the slice, just
|
||||||
|
// log that an update for it failed.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set existing emoji.
|
||||||
|
emojis[i] = existing
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
processingEmoji, err := d.GetRemoteEmoji(ctx, requestingUsername, e.ImageRemoteURL, e.Shortcode, e.Domain, newEmojiID, e.URI, &media.AdditionalEmojiInfo{
|
// Emojis changed!
|
||||||
Domain: &e.Domain,
|
changed = true
|
||||||
ImageRemoteURL: &e.ImageRemoteURL,
|
|
||||||
ImageStaticRemoteURL: &e.ImageStaticRemoteURL,
|
// Fetch this newly added emoji,
|
||||||
Disabled: e.Disabled,
|
// this function handles the case
|
||||||
VisibleInPicker: e.VisibleInPicker,
|
// of existing cached emojis and
|
||||||
}, refresh)
|
// new ones requiring dereference.
|
||||||
|
emoji, err := d.GetEmoji(ctx,
|
||||||
|
placeholder.Shortcode,
|
||||||
|
placeholder.Domain,
|
||||||
|
placeholder.ImageRemoteURL,
|
||||||
|
media.AdditionalEmojiInfo{
|
||||||
|
URI: &placeholder.URI,
|
||||||
|
ImageRemoteURL: &placeholder.ImageRemoteURL,
|
||||||
|
ImageStaticRemoteURL: &placeholder.ImageStaticRemoteURL,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(ctx, "couldn't get remote emoji %s: %s", shortcodeDomain, err)
|
if emoji == nil {
|
||||||
|
log.Errorf(ctx, "error loading emoji %s: %v", placeholder.ImageRemoteURL, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if gotEmoji, err = processingEmoji.LoadEmoji(ctx); err != nil {
|
// non-fatal error occurred during loading, still use it.
|
||||||
log.Errorf(ctx, "couldn't load remote emoji %s: %s", shortcodeDomain, err)
|
log.Warnf(ctx, "partially loaded emoji: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set updated emoji.
|
||||||
|
emojis[i] = emoji
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(emojis); {
|
||||||
|
if emojis[i].ID == "" {
|
||||||
|
// Remove failed emoji populations.
|
||||||
|
copy(emojis[i:], emojis[i+1:])
|
||||||
|
emojis = emojis[:len(emojis)-1]
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we get here, we either had the emoji already or we successfully fetched it
|
return emojis, changed, nil
|
||||||
gotEmojis = append(gotEmojis, gotEmoji)
|
|
||||||
}
|
|
||||||
|
|
||||||
return gotEmojis, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ package dereferencing_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -32,48 +33,50 @@ type EmojiTestSuite struct {
|
||||||
|
|
||||||
func (suite *EmojiTestSuite) TestDereferenceEmojiBlocking() {
|
func (suite *EmojiTestSuite) TestDereferenceEmojiBlocking() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
fetchingAccount := suite.testAccounts["local_account_1"]
|
|
||||||
emojiImageRemoteURL := "http://example.org/media/emojis/1781772.gif"
|
emojiImageRemoteURL := "http://example.org/media/emojis/1781772.gif"
|
||||||
emojiImageStaticRemoteURL := "http://example.org/media/emojis/1781772.gif"
|
emojiImageStaticRemoteURL := "http://example.org/media/emojis/1781772.gif"
|
||||||
emojiURI := "http://example.org/emojis/1781772"
|
emojiURI := "http://example.org/emojis/1781772"
|
||||||
emojiShortcode := "peglin"
|
emojiShortcode := "peglin"
|
||||||
emojiID := "01GCBMGNZBKMEE1KTZ6PMJEW5D"
|
|
||||||
emojiDomain := "example.org"
|
emojiDomain := "example.org"
|
||||||
emojiDisabled := false
|
emojiDisabled := false
|
||||||
emojiVisibleInPicker := false
|
emojiVisibleInPicker := false
|
||||||
|
|
||||||
ai := &media.AdditionalEmojiInfo{
|
emoji, err := suite.dereferencer.GetEmoji(
|
||||||
|
ctx,
|
||||||
|
emojiShortcode,
|
||||||
|
emojiDomain,
|
||||||
|
emojiImageRemoteURL,
|
||||||
|
media.AdditionalEmojiInfo{
|
||||||
|
URI: &emojiURI,
|
||||||
Domain: &emojiDomain,
|
Domain: &emojiDomain,
|
||||||
ImageRemoteURL: &emojiImageRemoteURL,
|
ImageRemoteURL: &emojiImageRemoteURL,
|
||||||
ImageStaticRemoteURL: &emojiImageStaticRemoteURL,
|
ImageStaticRemoteURL: &emojiImageStaticRemoteURL,
|
||||||
Disabled: &emojiDisabled,
|
Disabled: &emojiDisabled,
|
||||||
VisibleInPicker: &emojiVisibleInPicker,
|
VisibleInPicker: &emojiVisibleInPicker,
|
||||||
}
|
},
|
||||||
|
false,
|
||||||
processingEmoji, err := suite.dereferencer.GetRemoteEmoji(ctx, fetchingAccount.Username, emojiImageRemoteURL, emojiShortcode, emojiDomain, emojiID, emojiURI, ai, false)
|
)
|
||||||
suite.NoError(err)
|
|
||||||
|
|
||||||
// make a blocking call to load the emoji from the in-process media
|
|
||||||
emoji, err := processingEmoji.LoadEmoji(ctx)
|
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(emoji)
|
suite.NotNil(emoji)
|
||||||
|
|
||||||
suite.Equal(emojiID, emoji.ID)
|
expectPath := fmt.Sprintf("/emoji/original/%s.gif", emoji.ID)
|
||||||
|
expectStaticPath := fmt.Sprintf("/emoji/static/%s.png", emoji.ID)
|
||||||
|
|
||||||
suite.WithinDuration(time.Now(), emoji.CreatedAt, 10*time.Second)
|
suite.WithinDuration(time.Now(), emoji.CreatedAt, 10*time.Second)
|
||||||
suite.WithinDuration(time.Now(), emoji.UpdatedAt, 10*time.Second)
|
suite.WithinDuration(time.Now(), emoji.UpdatedAt, 10*time.Second)
|
||||||
suite.Equal(emojiShortcode, emoji.Shortcode)
|
suite.Equal(emojiShortcode, emoji.Shortcode)
|
||||||
suite.Equal(emojiDomain, emoji.Domain)
|
suite.Equal(emojiDomain, emoji.Domain)
|
||||||
suite.Equal(emojiImageRemoteURL, emoji.ImageRemoteURL)
|
suite.Equal(emojiImageRemoteURL, emoji.ImageRemoteURL)
|
||||||
suite.Equal(emojiImageStaticRemoteURL, emoji.ImageStaticRemoteURL)
|
suite.Equal(emojiImageStaticRemoteURL, emoji.ImageStaticRemoteURL)
|
||||||
suite.Contains(emoji.ImageURL, "/emoji/original/01GCBMGNZBKMEE1KTZ6PMJEW5D.gif")
|
suite.Contains(emoji.ImageURL, expectPath)
|
||||||
suite.Contains(emoji.ImageStaticURL, "emoji/static/01GCBMGNZBKMEE1KTZ6PMJEW5D.png")
|
suite.Contains(emoji.ImageStaticURL, expectStaticPath)
|
||||||
suite.Contains(emoji.ImagePath, "/emoji/original/01GCBMGNZBKMEE1KTZ6PMJEW5D.gif")
|
suite.Contains(emoji.ImagePath, expectPath)
|
||||||
suite.Contains(emoji.ImageStaticPath, "/emoji/static/01GCBMGNZBKMEE1KTZ6PMJEW5D.png")
|
suite.Contains(emoji.ImageStaticPath, expectStaticPath)
|
||||||
suite.Equal("image/gif", emoji.ImageContentType)
|
suite.Equal("image/gif", emoji.ImageContentType)
|
||||||
suite.Equal("image/png", emoji.ImageStaticContentType)
|
suite.Equal("image/png", emoji.ImageStaticContentType)
|
||||||
suite.Equal(37796, emoji.ImageFileSize)
|
suite.Equal(37796, emoji.ImageFileSize)
|
||||||
suite.Equal(7951, emoji.ImageStaticFileSize)
|
suite.Equal(7951, emoji.ImageStaticFileSize)
|
||||||
suite.WithinDuration(time.Now(), emoji.ImageUpdatedAt, 10*time.Second)
|
suite.WithinDuration(time.Now(), emoji.UpdatedAt, 10*time.Second)
|
||||||
suite.False(*emoji.Disabled)
|
suite.False(*emoji.Disabled)
|
||||||
suite.Equal(emojiURI, emoji.URI)
|
suite.Equal(emojiURI, emoji.URI)
|
||||||
suite.False(*emoji.VisibleInPicker)
|
suite.False(*emoji.VisibleInPicker)
|
||||||
|
|
|
@ -0,0 +1,215 @@
|
||||||
|
// GoToSocial
|
||||||
|
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package dereferencing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetMedia fetches the media at given remote URL by
|
||||||
|
// dereferencing it. The passed accountID is used to
|
||||||
|
// store it as being owned by that account. Additional
|
||||||
|
// information to set on the media attachment may also
|
||||||
|
// be provided.
|
||||||
|
//
|
||||||
|
// Please note that even if an error is returned,
|
||||||
|
// a media model may still be returned if the error
|
||||||
|
// was only encountered during actual dereferencing.
|
||||||
|
// In this case, it will act as a placeholder.
|
||||||
|
//
|
||||||
|
// Also note that since account / status dereferencing is
|
||||||
|
// already protected by per-uri locks, and that fediverse
|
||||||
|
// media is generally not shared between accounts (etc),
|
||||||
|
// there aren't any concurrency protections against multiple
|
||||||
|
// insertion / dereferencing of media at remoteURL. Worst
|
||||||
|
// case scenario, an extra media entry will be inserted
|
||||||
|
// and the scheduled cleaner.Cleaner{} will catch it!
|
||||||
|
func (d *Dereferencer) GetMedia(
|
||||||
|
ctx context.Context,
|
||||||
|
requestUser string,
|
||||||
|
accountID string, // media account owner
|
||||||
|
remoteURL string,
|
||||||
|
info media.AdditionalMediaInfo,
|
||||||
|
) (
|
||||||
|
*gtsmodel.MediaAttachment,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
// Parse str as valid URL object.
|
||||||
|
url, err := url.Parse(remoteURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.Newf("invalid remote media url %q: %v", remoteURL, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch transport for the provided request user from controller.
|
||||||
|
tsport, err := d.transportController.NewTransportForUsername(ctx,
|
||||||
|
requestUser,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.Newf("failed getting transport for %s: %w", requestUser, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start processing remote attachment at URL.
|
||||||
|
processing, err := d.mediaManager.CreateMedia(
|
||||||
|
ctx,
|
||||||
|
accountID,
|
||||||
|
func(ctx context.Context) (io.ReadCloser, int64, error) {
|
||||||
|
return tsport.DereferenceMedia(ctx, url)
|
||||||
|
},
|
||||||
|
info,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform media load operation.
|
||||||
|
media, err := processing.Load(ctx)
|
||||||
|
if err != nil {
|
||||||
|
err = gtserror.Newf("error loading media %s: %w", media.RemoteURL, err)
|
||||||
|
|
||||||
|
// TODO: in time we should return checkable flags by gtserror.Is___()
|
||||||
|
// which can determine if loading error should allow remaining placeholder.
|
||||||
|
}
|
||||||
|
|
||||||
|
return media, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshMedia ensures that given media is up-to-date,
|
||||||
|
// both in terms of being cached in local instance,
|
||||||
|
// storage and compared to extra info in information
|
||||||
|
// in given gtsmodel.AdditionMediaInfo{}. This handles
|
||||||
|
// the case of local emoji by returning early.
|
||||||
|
//
|
||||||
|
// Please note that even if an error is returned,
|
||||||
|
// a media model may still be returned if the error
|
||||||
|
// was only encountered during actual dereferencing.
|
||||||
|
// In this case, it will act as a placeholder.
|
||||||
|
//
|
||||||
|
// Also note that since account / status dereferencing is
|
||||||
|
// already protected by per-uri locks, and that fediverse
|
||||||
|
// media is generally not shared between accounts (etc),
|
||||||
|
// there aren't any concurrency protections against multiple
|
||||||
|
// insertion / dereferencing of media at remoteURL. Worst
|
||||||
|
// case scenario, an extra media entry will be inserted
|
||||||
|
// and the scheduled cleaner.Cleaner{} will catch it!
|
||||||
|
func (d *Dereferencer) RefreshMedia(
|
||||||
|
ctx context.Context,
|
||||||
|
requestUser string,
|
||||||
|
media *gtsmodel.MediaAttachment,
|
||||||
|
info media.AdditionalMediaInfo,
|
||||||
|
force bool,
|
||||||
|
) (
|
||||||
|
*gtsmodel.MediaAttachment,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
// Can't refresh local.
|
||||||
|
if media.IsLocal() {
|
||||||
|
return media, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check emoji is up-to-date
|
||||||
|
// with provided extra info.
|
||||||
|
switch {
|
||||||
|
case info.Blurhash != nil &&
|
||||||
|
*info.Blurhash != media.Blurhash:
|
||||||
|
force = true
|
||||||
|
case info.Description != nil &&
|
||||||
|
*info.Description != media.Description:
|
||||||
|
force = true
|
||||||
|
case info.RemoteURL != nil &&
|
||||||
|
*info.RemoteURL != media.RemoteURL:
|
||||||
|
force = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if needs updating.
|
||||||
|
if !force && *media.Cached {
|
||||||
|
return media, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: more finegrained freshness checks.
|
||||||
|
|
||||||
|
// Ensure we have a valid remote URL.
|
||||||
|
url, err := url.Parse(media.RemoteURL)
|
||||||
|
if err != nil {
|
||||||
|
err := gtserror.Newf("invalid media remote url %s: %w", media.RemoteURL, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch transport for the provided request user from controller.
|
||||||
|
tsport, err := d.transportController.NewTransportForUsername(ctx,
|
||||||
|
requestUser,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.Newf("failed getting transport for %s: %w", requestUser, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start processing remote attachment recache.
|
||||||
|
processing := d.mediaManager.RecacheMedia(
|
||||||
|
media,
|
||||||
|
func(ctx context.Context) (io.ReadCloser, int64, error) {
|
||||||
|
return tsport.DereferenceMedia(ctx, url)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Perform media load operation.
|
||||||
|
media, err = processing.Load(ctx)
|
||||||
|
if err != nil {
|
||||||
|
err = gtserror.Newf("error loading media %s: %w", media.RemoteURL, err)
|
||||||
|
|
||||||
|
// TODO: in time we should return checkable flags by gtserror.Is___()
|
||||||
|
// which can determine if loading error should allow remaining placeholder.
|
||||||
|
}
|
||||||
|
|
||||||
|
return media, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateAttachment handles the case of an existing media attachment
|
||||||
|
// that *may* have changes or need recaching. it checks for changed
|
||||||
|
// fields, updating in the database if so, and recaches uncached media.
|
||||||
|
func (d *Dereferencer) updateAttachment(
|
||||||
|
ctx context.Context,
|
||||||
|
requestUser string,
|
||||||
|
existing *gtsmodel.MediaAttachment, // existing attachment
|
||||||
|
attach *gtsmodel.MediaAttachment, // (optional) changed media
|
||||||
|
) (
|
||||||
|
*gtsmodel.MediaAttachment, // always set
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
var info media.AdditionalMediaInfo
|
||||||
|
|
||||||
|
if attach != nil {
|
||||||
|
// Set optional extra information,
|
||||||
|
// (will later check for changes).
|
||||||
|
info.Description = &attach.Description
|
||||||
|
info.Blurhash = &attach.Blurhash
|
||||||
|
info.RemoteURL = &attach.RemoteURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure media is cached.
|
||||||
|
return d.RefreshMedia(ctx,
|
||||||
|
requestUser,
|
||||||
|
existing,
|
||||||
|
info,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
|
@ -33,7 +33,6 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -536,12 +535,12 @@ func (d *Dereferencer) enrichStatus(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the status' media attachments are populated, passing in existing to check for changes.
|
// Ensure the status' media attachments are populated, passing in existing to check for changes.
|
||||||
if err := d.fetchStatusAttachments(ctx, tsport, status, latestStatus); err != nil {
|
if err := d.fetchStatusAttachments(ctx, requestUser, status, latestStatus); err != nil {
|
||||||
return nil, nil, gtserror.Newf("error populating attachments for status %s: %w", uri, err)
|
return nil, nil, gtserror.Newf("error populating attachments for status %s: %w", uri, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the status' emoji attachments are populated, (changes are expected / okay).
|
// Ensure the status' emoji attachments are populated, passing in existing to check for changes.
|
||||||
if err := d.fetchStatusEmojis(ctx, requestUser, latestStatus); err != nil {
|
if err := d.fetchStatusEmojis(ctx, status, latestStatus); err != nil {
|
||||||
return nil, nil, gtserror.Newf("error populating emojis for status %s: %w", uri, err)
|
return nil, nil, gtserror.Newf("error populating emojis for status %s: %w", uri, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -643,79 +642,12 @@ func (d *Dereferencer) isPermittedStatus(
|
||||||
return onFail()
|
return onFail()
|
||||||
}
|
}
|
||||||
|
|
||||||
// populateMentionTarget tries to populate the given
|
func (d *Dereferencer) fetchStatusMentions(
|
||||||
// mention with the correct TargetAccount and (if not
|
|
||||||
// yet set) TargetAccountURI, returning the populated
|
|
||||||
// mention.
|
|
||||||
//
|
|
||||||
// Will check on the existing status if the mention
|
|
||||||
// is already there and populated; if so, existing
|
|
||||||
// mention will be returned along with `true`.
|
|
||||||
//
|
|
||||||
// Otherwise, this function will try to parse first
|
|
||||||
// the Href of the mention, and then the namestring,
|
|
||||||
// to see who it targets, and go fetch that account.
|
|
||||||
func (d *Dereferencer) populateMentionTarget(
|
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
mention *gtsmodel.Mention,
|
|
||||||
requestUser string,
|
requestUser string,
|
||||||
existing, status *gtsmodel.Status,
|
existing *gtsmodel.Status,
|
||||||
) (
|
status *gtsmodel.Status,
|
||||||
*gtsmodel.Mention,
|
) error {
|
||||||
bool, // True if mention already exists in the DB.
|
|
||||||
error,
|
|
||||||
) {
|
|
||||||
// Mentions can be created using Name or Href.
|
|
||||||
// Prefer Href (TargetAccountURI), fall back to Name.
|
|
||||||
if mention.TargetAccountURI != "" {
|
|
||||||
// Look for existing mention with this URI.
|
|
||||||
// If we already have it we can return early.
|
|
||||||
existingMention, ok := existing.GetMentionByTargetURI(mention.TargetAccountURI)
|
|
||||||
if ok && existingMention.ID != "" {
|
|
||||||
return existingMention, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that mention account URI is parseable.
|
|
||||||
accountURI, err := url.Parse(mention.TargetAccountURI)
|
|
||||||
if err != nil {
|
|
||||||
err = gtserror.Newf("invalid account uri %q: %w", mention.TargetAccountURI, err)
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure we have the account of the mention target dereferenced.
|
|
||||||
mention.TargetAccount, _, err = d.getAccountByURI(ctx, requestUser, accountURI)
|
|
||||||
if err != nil {
|
|
||||||
err = gtserror.Newf("failed to dereference account %s: %w", accountURI, err)
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Href wasn't set. Find the target account using namestring.
|
|
||||||
username, domain, err := util.ExtractNamestringParts(mention.NameString)
|
|
||||||
if err != nil {
|
|
||||||
err = gtserror.Newf("failed to parse namestring %s: %w", mention.NameString, err)
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
mention.TargetAccount, _, err = d.getAccountByUsernameDomain(ctx, requestUser, username, domain)
|
|
||||||
if err != nil {
|
|
||||||
err = gtserror.Newf("failed to dereference account %s: %w", mention.NameString, err)
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for existing mention with this URI.
|
|
||||||
mention.TargetAccountURI = mention.TargetAccount.URI
|
|
||||||
existingMention, ok := existing.GetMentionByTargetURI(mention.TargetAccountURI)
|
|
||||||
if ok && existingMention.ID != "" {
|
|
||||||
return existingMention, true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// At this point, mention.TargetAccountURI
|
|
||||||
// and mention.TargetAccount must be set.
|
|
||||||
return mention, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Dereferencer) fetchStatusMentions(ctx context.Context, requestUser string, existing, status *gtsmodel.Status) error {
|
|
||||||
// Allocate new slice to take the yet-to-be created mention IDs.
|
// Allocate new slice to take the yet-to-be created mention IDs.
|
||||||
status.MentionIDs = make([]string, len(status.Mentions))
|
status.MentionIDs = make([]string, len(status.Mentions))
|
||||||
|
|
||||||
|
@ -728,10 +660,10 @@ func (d *Dereferencer) fetchStatusMentions(ctx context.Context, requestUser stri
|
||||||
|
|
||||||
mention, alreadyExists, err = d.populateMentionTarget(
|
mention, alreadyExists, err = d.populateMentionTarget(
|
||||||
ctx,
|
ctx,
|
||||||
mention,
|
|
||||||
requestUser,
|
requestUser,
|
||||||
existing,
|
existing,
|
||||||
status,
|
status,
|
||||||
|
mention,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(ctx, "failed to derive mention: %v", err)
|
log.Errorf(ctx, "failed to derive mention: %v", err)
|
||||||
|
@ -845,7 +777,11 @@ func (d *Dereferencer) threadStatus(ctx context.Context, status *gtsmodel.Status
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Dereferencer) fetchStatusTags(ctx context.Context, existing, status *gtsmodel.Status) error {
|
func (d *Dereferencer) fetchStatusTags(
|
||||||
|
ctx context.Context,
|
||||||
|
existing *gtsmodel.Status,
|
||||||
|
status *gtsmodel.Status,
|
||||||
|
) error {
|
||||||
// Allocate new slice to take the yet-to-be determined tag IDs.
|
// Allocate new slice to take the yet-to-be determined tag IDs.
|
||||||
status.TagIDs = make([]string, len(status.Tags))
|
status.TagIDs = make([]string, len(status.Tags))
|
||||||
|
|
||||||
|
@ -900,7 +836,11 @@ func (d *Dereferencer) fetchStatusTags(ctx context.Context, existing, status *gt
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Dereferencer) fetchStatusPoll(ctx context.Context, existing, status *gtsmodel.Status) error {
|
func (d *Dereferencer) fetchStatusPoll(
|
||||||
|
ctx context.Context,
|
||||||
|
existing *gtsmodel.Status,
|
||||||
|
status *gtsmodel.Status,
|
||||||
|
) error {
|
||||||
var (
|
var (
|
||||||
// insertStatusPoll generates ID and inserts the poll attached to status into the database.
|
// insertStatusPoll generates ID and inserts the poll attached to status into the database.
|
||||||
insertStatusPoll = func(ctx context.Context, status *gtsmodel.Status) error {
|
insertStatusPoll = func(ctx context.Context, status *gtsmodel.Status) error {
|
||||||
|
@ -990,19 +930,24 @@ func (d *Dereferencer) fetchStatusPoll(ctx context.Context, existing, status *gt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Dereferencer) fetchStatusAttachments(ctx context.Context, tsport transport.Transport, existing, status *gtsmodel.Status) error {
|
func (d *Dereferencer) fetchStatusAttachments(
|
||||||
|
ctx context.Context,
|
||||||
|
requestUser string,
|
||||||
|
existing *gtsmodel.Status,
|
||||||
|
status *gtsmodel.Status,
|
||||||
|
) error {
|
||||||
// Allocate new slice to take the yet-to-be fetched attachment IDs.
|
// Allocate new slice to take the yet-to-be fetched attachment IDs.
|
||||||
status.AttachmentIDs = make([]string, len(status.Attachments))
|
status.AttachmentIDs = make([]string, len(status.Attachments))
|
||||||
|
|
||||||
for i := range status.Attachments {
|
for i := range status.Attachments {
|
||||||
attachment := status.Attachments[i]
|
placeholder := status.Attachments[i]
|
||||||
|
|
||||||
// Look for existing media attachment with remote URL first.
|
// Look for existing media attachment with remote URL first.
|
||||||
existing, ok := existing.GetAttachmentByRemoteURL(attachment.RemoteURL)
|
existing, ok := existing.GetAttachmentByRemoteURL(placeholder.RemoteURL)
|
||||||
if ok && existing.ID != "" {
|
if ok && existing.ID != "" {
|
||||||
|
|
||||||
// Ensure the existing media attachment is up-to-date and cached.
|
// Ensure the existing media attachment is up-to-date and cached.
|
||||||
existing, err := d.updateAttachment(ctx, tsport, existing, attachment)
|
existing, err := d.updateAttachment(ctx, requestUser, existing, placeholder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(ctx, "error updating existing attachment: %v", err)
|
log.Errorf(ctx, "error updating existing attachment: %v", err)
|
||||||
|
|
||||||
|
@ -1019,25 +964,25 @@ func (d *Dereferencer) fetchStatusAttachments(ctx context.Context, tsport transp
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load this new media attachment.
|
// Load this new media attachment.
|
||||||
attachment, err := d.loadAttachment(
|
attachment, err := d.GetMedia(
|
||||||
ctx,
|
ctx,
|
||||||
tsport,
|
requestUser,
|
||||||
status.AccountID,
|
status.AccountID,
|
||||||
attachment.RemoteURL,
|
placeholder.RemoteURL,
|
||||||
&media.AdditionalMediaInfo{
|
media.AdditionalMediaInfo{
|
||||||
StatusID: &status.ID,
|
StatusID: &status.ID,
|
||||||
RemoteURL: &attachment.RemoteURL,
|
RemoteURL: &placeholder.RemoteURL,
|
||||||
Description: &attachment.Description,
|
Description: &placeholder.Description,
|
||||||
Blurhash: &attachment.Blurhash,
|
Blurhash: &placeholder.Blurhash,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil && attachment == nil {
|
if err != nil {
|
||||||
log.Errorf(ctx, "error loading attachment: %v", err)
|
if attachment == nil {
|
||||||
|
log.Errorf(ctx, "error loading attachment %s: %v", placeholder.RemoteURL, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
// non-fatal error occurred during loading, still use it.
|
||||||
// A non-fatal error occurred during loading.
|
|
||||||
log.Warnf(ctx, "partially loaded attachment: %v", err)
|
log.Warnf(ctx, "partially loaded attachment: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1061,22 +1006,108 @@ func (d *Dereferencer) fetchStatusAttachments(ctx context.Context, tsport transp
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Dereferencer) fetchStatusEmojis(ctx context.Context, requestUser string, status *gtsmodel.Status) error {
|
func (d *Dereferencer) fetchStatusEmojis(
|
||||||
// Fetch the full-fleshed-out emoji objects for our status.
|
ctx context.Context,
|
||||||
emojis, err := d.populateEmojis(ctx, status.Emojis, requestUser)
|
existing *gtsmodel.Status,
|
||||||
|
status *gtsmodel.Status,
|
||||||
|
) error {
|
||||||
|
// Fetch the updated emojis for our status.
|
||||||
|
emojis, changed, err := d.fetchEmojis(ctx,
|
||||||
|
existing.Emojis,
|
||||||
|
status.Emojis,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gtserror.Newf("failed to populate emojis: %w", err)
|
return gtserror.Newf("error fetching emojis: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterate over and get their IDs.
|
if !changed {
|
||||||
emojiIDs := make([]string, 0, len(emojis))
|
// Use existing status emoji objects.
|
||||||
for _, e := range emojis {
|
status.EmojiIDs = existing.EmojiIDs
|
||||||
emojiIDs = append(emojiIDs, e.ID)
|
status.Emojis = existing.Emojis
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set known emoji details.
|
// Set latest emojis.
|
||||||
status.Emojis = emojis
|
status.Emojis = emojis
|
||||||
status.EmojiIDs = emojiIDs
|
|
||||||
|
// Iterate over and set changed emoji IDs.
|
||||||
|
status.EmojiIDs = make([]string, len(emojis))
|
||||||
|
for i, emoji := range emojis {
|
||||||
|
status.EmojiIDs[i] = emoji.ID
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// populateMentionTarget tries to populate the given
|
||||||
|
// mention with the correct TargetAccount and (if not
|
||||||
|
// yet set) TargetAccountURI, returning the populated
|
||||||
|
// mention.
|
||||||
|
//
|
||||||
|
// Will check on the existing status if the mention
|
||||||
|
// is already there and populated; if so, existing
|
||||||
|
// mention will be returned along with `true`.
|
||||||
|
//
|
||||||
|
// Otherwise, this function will try to parse first
|
||||||
|
// the Href of the mention, and then the namestring,
|
||||||
|
// to see who it targets, and go fetch that account.
|
||||||
|
func (d *Dereferencer) populateMentionTarget(
|
||||||
|
ctx context.Context,
|
||||||
|
requestUser string,
|
||||||
|
existing *gtsmodel.Status,
|
||||||
|
status *gtsmodel.Status,
|
||||||
|
mention *gtsmodel.Mention,
|
||||||
|
) (
|
||||||
|
*gtsmodel.Mention,
|
||||||
|
bool, // True if mention already exists in the DB.
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
// Mentions can be created using Name or Href.
|
||||||
|
// Prefer Href (TargetAccountURI), fall back to Name.
|
||||||
|
if mention.TargetAccountURI != "" {
|
||||||
|
// Look for existing mention with this URI.
|
||||||
|
// If we already have it we can return early.
|
||||||
|
existingMention, ok := existing.GetMentionByTargetURI(mention.TargetAccountURI)
|
||||||
|
if ok && existingMention.ID != "" {
|
||||||
|
return existingMention, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that mention account URI is parseable.
|
||||||
|
accountURI, err := url.Parse(mention.TargetAccountURI)
|
||||||
|
if err != nil {
|
||||||
|
err = gtserror.Newf("invalid account uri %q: %w", mention.TargetAccountURI, err)
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we have the account of the mention target dereferenced.
|
||||||
|
mention.TargetAccount, _, err = d.getAccountByURI(ctx, requestUser, accountURI)
|
||||||
|
if err != nil {
|
||||||
|
err = gtserror.Newf("failed to dereference account %s: %w", accountURI, err)
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Href wasn't set. Find the target account using namestring.
|
||||||
|
username, domain, err := util.ExtractNamestringParts(mention.NameString)
|
||||||
|
if err != nil {
|
||||||
|
err = gtserror.Newf("failed to parse namestring %s: %w", mention.NameString, err)
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mention.TargetAccount, _, err = d.getAccountByUsernameDomain(ctx, requestUser, username, domain)
|
||||||
|
if err != nil {
|
||||||
|
err = gtserror.Newf("failed to dereference account %s: %w", mention.NameString, err)
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for existing mention with this URI.
|
||||||
|
mention.TargetAccountURI = mention.TargetAccount.URI
|
||||||
|
existingMention, ok := existing.GetMentionByTargetURI(mention.TargetAccountURI)
|
||||||
|
if ok && existingMention.ID != "" {
|
||||||
|
return existingMention, true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, mention.TargetAccountURI
|
||||||
|
// and mention.TargetAccount must be set.
|
||||||
|
return mention, false, nil
|
||||||
|
}
|
||||||
|
|
|
@ -18,120 +18,36 @@
|
||||||
package dereferencing
|
package dereferencing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"net/url"
|
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// loadAttachment handles the case of a new media attachment
|
// getEmojiByShortcodeDomain searches input slice
|
||||||
// that requires loading. it stores and caches from given data.
|
// for emoji with given shortcode and domain.
|
||||||
func (d *Dereferencer) loadAttachment(
|
func getEmojiByShortcodeDomain(
|
||||||
ctx context.Context,
|
emojis []*gtsmodel.Emoji,
|
||||||
tsport transport.Transport,
|
shortcode string,
|
||||||
accountID string, // media account owner
|
domain string,
|
||||||
remoteURL string,
|
|
||||||
info *media.AdditionalMediaInfo,
|
|
||||||
) (
|
) (
|
||||||
*gtsmodel.MediaAttachment,
|
*gtsmodel.Emoji,
|
||||||
error,
|
bool,
|
||||||
) {
|
) {
|
||||||
// Parse str as valid URL object.
|
for _, emoji := range emojis {
|
||||||
url, err := url.Parse(remoteURL)
|
if emoji.Shortcode == shortcode &&
|
||||||
if err != nil {
|
emoji.Domain == domain {
|
||||||
return nil, gtserror.Newf("invalid remote media url %q: %v", remoteURL, err)
|
return emoji, true
|
||||||
}
|
|
||||||
|
|
||||||
// Start pre-processing remote media at remote URL.
|
|
||||||
processing := d.mediaManager.PreProcessMedia(
|
|
||||||
func(ctx context.Context) (io.ReadCloser, int64, error) {
|
|
||||||
return tsport.DereferenceMedia(ctx, url)
|
|
||||||
},
|
|
||||||
accountID,
|
|
||||||
info,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Force attachment loading *right now*.
|
|
||||||
return processing.LoadAttachment(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateAttachment handles the case of an existing media attachment
|
|
||||||
// that *may* have changes or need recaching. it checks for changed
|
|
||||||
// fields, updating in the database if so, and recaches uncached media.
|
|
||||||
func (d *Dereferencer) updateAttachment(
|
|
||||||
ctx context.Context,
|
|
||||||
tsport transport.Transport,
|
|
||||||
existing *gtsmodel.MediaAttachment, // existing attachment
|
|
||||||
media *gtsmodel.MediaAttachment, // (optional) changed media
|
|
||||||
) (
|
|
||||||
*gtsmodel.MediaAttachment, // always set
|
|
||||||
error,
|
|
||||||
) {
|
|
||||||
if media != nil {
|
|
||||||
// Possible changed media columns.
|
|
||||||
changed := make([]string, 0, 3)
|
|
||||||
|
|
||||||
// Check if attachment description has changed.
|
|
||||||
if existing.Description != media.Description {
|
|
||||||
changed = append(changed, "description")
|
|
||||||
existing.Description = media.Description
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if attachment blurhash has changed (i.e. content change).
|
|
||||||
if existing.Blurhash != media.Blurhash && media.Blurhash != "" {
|
|
||||||
changed = append(changed, "blurhash", "cached")
|
|
||||||
existing.Blurhash = media.Blurhash
|
|
||||||
existing.Cached = util.Ptr(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(changed) > 0 {
|
|
||||||
// Update the existing attachment model in the database.
|
|
||||||
err := d.state.DB.UpdateAttachment(ctx, existing, changed...)
|
|
||||||
if err != nil {
|
|
||||||
return media, gtserror.Newf("error updating media: %w", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if cached.
|
// emojiChanged returns whether an emoji has changed in a way
|
||||||
if *existing.Cached {
|
// that indicates that it should be refetched and refreshed.
|
||||||
return existing, nil
|
func emojiChanged(existing, latest *gtsmodel.Emoji) bool {
|
||||||
}
|
return existing.URI != latest.URI ||
|
||||||
|
existing.ImageRemoteURL != latest.ImageRemoteURL ||
|
||||||
// Parse str as valid URL object.
|
existing.ImageStaticRemoteURL != latest.ImageStaticRemoteURL
|
||||||
url, err := url.Parse(existing.RemoteURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.Newf("invalid remote media url %q: %v", media.RemoteURL, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start pre-processing remote media recaching from remote.
|
|
||||||
processing, err := d.mediaManager.PreProcessMediaRecache(
|
|
||||||
ctx,
|
|
||||||
func(ctx context.Context) (io.ReadCloser, int64, error) {
|
|
||||||
return tsport.DereferenceMedia(ctx, url)
|
|
||||||
},
|
|
||||||
existing.ID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.Newf("error processing recache: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force load attachment recache *right now*.
|
|
||||||
recached, err := processing.LoadAttachment(ctx)
|
|
||||||
|
|
||||||
// Always return the error we
|
|
||||||
// receive, but ensure we return
|
|
||||||
// most up-to-date media file.
|
|
||||||
if recached != nil {
|
|
||||||
return recached, err
|
|
||||||
}
|
|
||||||
return existing, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// pollChanged returns whether a poll has changed in way that
|
// pollChanged returns whether a poll has changed in way that
|
||||||
|
|
|
@ -30,19 +30,18 @@ type Emoji struct {
|
||||||
ImageStaticRemoteURL string `bun:",nullzero"` // Where can a static / non-animated version of this emoji be retrieved remotely? Null for local emojis.
|
ImageStaticRemoteURL string `bun:",nullzero"` // Where can a static / non-animated version of this emoji be retrieved remotely? Null for local emojis.
|
||||||
ImageURL string `bun:",nullzero"` // Where can this emoji be retrieved from the local server? Null for remote emojis.
|
ImageURL string `bun:",nullzero"` // Where can this emoji be retrieved from the local server? Null for remote emojis.
|
||||||
ImageStaticURL string `bun:",nullzero"` // Where can a static version of this emoji be retrieved from the local server? Null for remote emojis.
|
ImageStaticURL string `bun:",nullzero"` // Where can a static version of this emoji be retrieved from the local server? Null for remote emojis.
|
||||||
ImagePath string `bun:",nullzero,notnull"` // Path of the emoji image in the server storage system.
|
ImagePath string `bun:",notnull"` // Path of the emoji image in the server storage system.
|
||||||
ImageStaticPath string `bun:",nullzero,notnull"` // Path of a static version of the emoji image in the server storage system
|
ImageStaticPath string `bun:",notnull"` // Path of a static version of the emoji image in the server storage system
|
||||||
ImageContentType string `bun:",nullzero,notnull"` // MIME content type of the emoji image
|
ImageContentType string `bun:",notnull"` // MIME content type of the emoji image
|
||||||
ImageStaticContentType string `bun:",nullzero,notnull"` // MIME content type of the static version of the emoji image.
|
ImageStaticContentType string `bun:",notnull"` // MIME content type of the static version of the emoji image.
|
||||||
ImageFileSize int `bun:",nullzero,notnull"` // Size of the emoji image file in bytes, for serving purposes.
|
ImageFileSize int `bun:",notnull"` // Size of the emoji image file in bytes, for serving purposes.
|
||||||
ImageStaticFileSize int `bun:",nullzero,notnull"` // Size of the static version of the emoji image file in bytes, for serving purposes.
|
ImageStaticFileSize int `bun:",notnull"` // Size of the static version of the emoji image file in bytes, for serving purposes.
|
||||||
ImageUpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // When was the emoji image last updated?
|
|
||||||
Disabled *bool `bun:",nullzero,notnull,default:false"` // Has a moderation action disabled this emoji from being shown?
|
Disabled *bool `bun:",nullzero,notnull,default:false"` // Has a moderation action disabled this emoji from being shown?
|
||||||
URI string `bun:",nullzero,notnull,unique"` // ActivityPub uri of this emoji. Something like 'https://example.org/emojis/1234'
|
URI string `bun:",nullzero,notnull,unique"` // ActivityPub uri of this emoji. Something like 'https://example.org/emojis/1234'
|
||||||
VisibleInPicker *bool `bun:",nullzero,notnull,default:true"` // Is this emoji visible in the admin emoji picker?
|
VisibleInPicker *bool `bun:",nullzero,notnull,default:true"` // Is this emoji visible in the admin emoji picker?
|
||||||
Category *EmojiCategory `bun:"rel:belongs-to"` // In which emoji category is this emoji visible?
|
Category *EmojiCategory `bun:"rel:belongs-to"` // In which emoji category is this emoji visible?
|
||||||
CategoryID string `bun:"type:CHAR(26),nullzero"` // ID of the category this emoji belongs to.
|
CategoryID string `bun:"type:CHAR(26),nullzero"` // ID of the category this emoji belongs to.
|
||||||
Cached *bool `bun:",nullzero,notnull,default:false"`
|
Cached *bool `bun:",nullzero,notnull,default:false"` // whether emoji is cached in locally in gotosocial storage.
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsLocal returns true if the emoji is
|
// IsLocal returns true if the emoji is
|
||||||
|
|
|
@ -30,8 +30,8 @@ type MediaAttachment struct {
|
||||||
StatusID string `bun:"type:CHAR(26),nullzero"` // ID of the status to which this is attached
|
StatusID string `bun:"type:CHAR(26),nullzero"` // ID of the status to which this is attached
|
||||||
URL string `bun:",nullzero"` // Where can the attachment be retrieved on *this* server
|
URL string `bun:",nullzero"` // Where can the attachment be retrieved on *this* server
|
||||||
RemoteURL string `bun:",nullzero"` // Where can the attachment be retrieved on a remote server (empty for local media)
|
RemoteURL string `bun:",nullzero"` // Where can the attachment be retrieved on a remote server (empty for local media)
|
||||||
Type FileType `bun:",nullzero,notnull"` // Type of file (image/gifv/audio/video/unknown)
|
Type FileType `bun:",notnull"` // Type of file (image/gifv/audio/video/unknown)
|
||||||
FileMeta FileMeta `bun:",embed:,nullzero,notnull"` // Metadata about the file
|
FileMeta FileMeta `bun:",embed:,notnull"` // Metadata about the file
|
||||||
AccountID string `bun:"type:CHAR(26),nullzero,notnull"` // To which account does this attachment belong
|
AccountID string `bun:"type:CHAR(26),nullzero,notnull"` // To which account does this attachment belong
|
||||||
Description string `bun:""` // Description of the attachment (for screenreaders)
|
Description string `bun:""` // Description of the attachment (for screenreaders)
|
||||||
ScheduledStatusID string `bun:"type:CHAR(26),nullzero"` // To which scheduled status does this attachment belong
|
ScheduledStatusID string `bun:"type:CHAR(26),nullzero"` // To which scheduled status does this attachment belong
|
||||||
|
@ -44,20 +44,28 @@ type MediaAttachment struct {
|
||||||
Cached *bool `bun:",nullzero,notnull,default:false"` // Is this attachment currently cached by our instance?
|
Cached *bool `bun:",nullzero,notnull,default:false"` // Is this attachment currently cached by our instance?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsLocal returns whether media attachment is local.
|
||||||
|
func (m *MediaAttachment) IsLocal() bool {
|
||||||
|
return m.RemoteURL == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRemote returns whether media attachment is remote.
|
||||||
|
func (m *MediaAttachment) IsRemote() bool {
|
||||||
|
return m.RemoteURL != ""
|
||||||
|
}
|
||||||
|
|
||||||
// File refers to the metadata for the whole file
|
// File refers to the metadata for the whole file
|
||||||
type File struct {
|
type File struct {
|
||||||
Path string `bun:",nullzero,notnull"` // Path of the file in storage.
|
Path string `bun:",notnull"` // Path of the file in storage.
|
||||||
ContentType string `bun:",nullzero,notnull"` // MIME content type of the file.
|
ContentType string `bun:",notnull"` // MIME content type of the file.
|
||||||
FileSize int `bun:",notnull"` // File size in bytes
|
FileSize int `bun:",notnull"` // File size in bytes
|
||||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // When was the file last updated.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Thumbnail refers to a small image thumbnail derived from a larger image, video, or audio file.
|
// Thumbnail refers to a small image thumbnail derived from a larger image, video, or audio file.
|
||||||
type Thumbnail struct {
|
type Thumbnail struct {
|
||||||
Path string `bun:",nullzero,notnull"` // Path of the file in storage.
|
Path string `bun:",notnull"` // Path of the file in storage.
|
||||||
ContentType string `bun:",nullzero,notnull"` // MIME content type of the file.
|
ContentType string `bun:",notnull"` // MIME content type of the file.
|
||||||
FileSize int `bun:",notnull"` // File size in bytes
|
FileSize int `bun:",notnull"` // File size in bytes
|
||||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // When was the file last updated.
|
|
||||||
URL string `bun:",nullzero"` // What is the URL of the thumbnail on the local server
|
URL string `bun:",nullzero"` // What is the URL of the thumbnail on the local server
|
||||||
RemoteURL string `bun:",nullzero"` // What is the remote URL of the thumbnail (empty for local media)
|
RemoteURL string `bun:",nullzero"` // What is the remote URL of the thumbnail (empty for local media)
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,12 +43,9 @@ var (
|
||||||
BufferPool: &pngEncoderBufferPool{},
|
BufferPool: &pngEncoderBufferPool{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// jpegBufferPool is a memory pool of byte buffers for JPEG encoding.
|
// jpegBufferPool is a memory pool
|
||||||
jpegBufferPool = sync.Pool{
|
// of byte buffers for JPEG encoding.
|
||||||
New: func() any {
|
jpegBufferPool sync.Pool
|
||||||
return bufio.NewWriter(nil)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// gtsImage is a thin wrapper around the standard library image
|
// gtsImage is a thin wrapper around the standard library image
|
||||||
|
@ -80,25 +77,29 @@ func decodeImage(r io.Reader, opts ...imaging.DecodeOption) (*gtsImage, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Width returns the image width in pixels.
|
// Width returns the image width in pixels.
|
||||||
func (m *gtsImage) Width() uint32 {
|
func (m *gtsImage) Width() int {
|
||||||
return uint32(m.image.Bounds().Size().X)
|
return m.image.Bounds().Size().X
|
||||||
}
|
}
|
||||||
|
|
||||||
// Height returns the image height in pixels.
|
// Height returns the image height in pixels.
|
||||||
func (m *gtsImage) Height() uint32 {
|
func (m *gtsImage) Height() int {
|
||||||
return uint32(m.image.Bounds().Size().Y)
|
return m.image.Bounds().Size().Y
|
||||||
}
|
}
|
||||||
|
|
||||||
// Size returns the total number of image pixels.
|
// Size returns the total number of image pixels.
|
||||||
func (m *gtsImage) Size() uint64 {
|
func (m *gtsImage) Size() int {
|
||||||
return uint64(m.image.Bounds().Size().X) *
|
return m.image.Bounds().Size().X *
|
||||||
uint64(m.image.Bounds().Size().Y)
|
m.image.Bounds().Size().Y
|
||||||
}
|
}
|
||||||
|
|
||||||
// AspectRatio returns the image ratio of width:height.
|
// AspectRatio returns the image ratio of width:height.
|
||||||
func (m *gtsImage) AspectRatio() float32 {
|
func (m *gtsImage) AspectRatio() float32 {
|
||||||
return float32(m.image.Bounds().Size().X) /
|
|
||||||
float32(m.image.Bounds().Size().Y)
|
// note: we cast bounds to float64 to prevent truncation
|
||||||
|
// and only at the end aspect ratio do we cast to float32
|
||||||
|
// (as the sizes are likely to be much larger than ratio).
|
||||||
|
return float32(float64(m.image.Bounds().Size().X) /
|
||||||
|
float64(m.image.Bounds().Size().Y))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Thumbnail returns a small sized copy of gtsImage{}, limited to 512x512 if not small enough.
|
// Thumbnail returns a small sized copy of gtsImage{}, limited to 512x512 if not small enough.
|
||||||
|
@ -160,7 +161,11 @@ func (m *gtsImage) ToPNG() io.Reader {
|
||||||
|
|
||||||
// getJPEGBuffer fetches a reset JPEG encoding buffer from global JPEG buffer pool.
|
// getJPEGBuffer fetches a reset JPEG encoding buffer from global JPEG buffer pool.
|
||||||
func getJPEGBuffer(w io.Writer) *bufio.Writer {
|
func getJPEGBuffer(w io.Writer) *bufio.Writer {
|
||||||
buf, _ := jpegBufferPool.Get().(*bufio.Writer)
|
v := jpegBufferPool.Get()
|
||||||
|
if v == nil {
|
||||||
|
v = bufio.NewWriter(nil)
|
||||||
|
}
|
||||||
|
buf := v.(*bufio.Writer)
|
||||||
buf.Reset(w)
|
buf.Reset(w)
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,176 +56,172 @@ func NewManager(state *state.State) *Manager {
|
||||||
return &Manager{state: state}
|
return &Manager{state: state}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PreProcessMedia begins the process of decoding
|
// CreateMedia creates a new media attachment entry
|
||||||
// and storing the given data as an attachment.
|
// in the database for given owning account ID and
|
||||||
// It will return a pointer to a ProcessingMedia
|
// extra information, and prepares a new processing
|
||||||
// struct upon which further actions can be performed,
|
// media entry to dereference it using the given
|
||||||
// such as getting the finished media, thumbnail,
|
// data function, decode the media and finish filling
|
||||||
// attachment, etc.
|
// out remaining media fields (e.g. type, path, etc).
|
||||||
//
|
func (m *Manager) CreateMedia(
|
||||||
// - data: a function that the media manager can call
|
ctx context.Context,
|
||||||
// to return a reader containing the media data.
|
|
||||||
// - accountID: the account that the media belongs to.
|
|
||||||
// - ai: optional and can be nil. Any additional information
|
|
||||||
// about the attachment provided will be put in the database.
|
|
||||||
//
|
|
||||||
// Note: unlike ProcessMedia, this will NOT
|
|
||||||
// queue the media to be asynchronously processed.
|
|
||||||
func (m *Manager) PreProcessMedia(
|
|
||||||
data DataFunc,
|
|
||||||
accountID string,
|
accountID string,
|
||||||
ai *AdditionalMediaInfo,
|
data DataFunc,
|
||||||
) *ProcessingMedia {
|
info AdditionalMediaInfo,
|
||||||
|
) (
|
||||||
|
*ProcessingMedia,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// Generate new ID.
|
||||||
|
id := id.NewULID()
|
||||||
|
|
||||||
|
// Placeholder URL for attachment.
|
||||||
|
url := uris.URIForAttachment(
|
||||||
|
accountID,
|
||||||
|
string(TypeAttachment),
|
||||||
|
string(SizeOriginal),
|
||||||
|
id,
|
||||||
|
"unknown",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Placeholder storage path for attachment.
|
||||||
|
path := uris.StoragePathForAttachment(
|
||||||
|
accountID,
|
||||||
|
string(TypeAttachment),
|
||||||
|
string(SizeOriginal),
|
||||||
|
id,
|
||||||
|
"unknown",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Calculate attachment thumbnail file path
|
||||||
|
thumbPath := uris.StoragePathForAttachment(
|
||||||
|
accountID,
|
||||||
|
string(TypeAttachment),
|
||||||
|
string(SizeSmall),
|
||||||
|
id,
|
||||||
|
|
||||||
|
// Always encode attachment
|
||||||
|
// thumbnails as jpg.
|
||||||
|
"jpg",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Calculate attachment thumbnail URL.
|
||||||
|
thumbURL := uris.URIForAttachment(
|
||||||
|
accountID,
|
||||||
|
string(TypeAttachment),
|
||||||
|
string(SizeSmall),
|
||||||
|
id,
|
||||||
|
|
||||||
|
// Always encode attachment
|
||||||
|
// thumbnails as jpg.
|
||||||
|
"jpg",
|
||||||
|
)
|
||||||
|
|
||||||
// Populate initial fields on the new media,
|
// Populate initial fields on the new media,
|
||||||
// leaving out fields with values we don't know
|
// leaving out fields with values we don't know
|
||||||
// yet. These will be overwritten as we go.
|
// yet. These will be overwritten as we go.
|
||||||
now := time.Now()
|
|
||||||
attachment := >smodel.MediaAttachment{
|
attachment := >smodel.MediaAttachment{
|
||||||
ID: id.NewULID(),
|
ID: id,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
|
URL: url,
|
||||||
Type: gtsmodel.FileTypeUnknown,
|
Type: gtsmodel.FileTypeUnknown,
|
||||||
FileMeta: gtsmodel.FileMeta{},
|
|
||||||
AccountID: accountID,
|
AccountID: accountID,
|
||||||
Processing: gtsmodel.ProcessingStatusReceived,
|
Processing: gtsmodel.ProcessingStatusReceived,
|
||||||
File: gtsmodel.File{
|
File: gtsmodel.File{
|
||||||
UpdatedAt: now,
|
|
||||||
ContentType: "application/octet-stream",
|
ContentType: "application/octet-stream",
|
||||||
|
Path: path,
|
||||||
|
},
|
||||||
|
Thumbnail: gtsmodel.Thumbnail{
|
||||||
|
ContentType: mimeImageJpeg, // thumbs always jpg.
|
||||||
|
Path: thumbPath,
|
||||||
|
URL: thumbURL,
|
||||||
},
|
},
|
||||||
Thumbnail: gtsmodel.Thumbnail{UpdatedAt: now},
|
|
||||||
Avatar: util.Ptr(false),
|
Avatar: util.Ptr(false),
|
||||||
Header: util.Ptr(false),
|
Header: util.Ptr(false),
|
||||||
Cached: util.Ptr(false),
|
Cached: util.Ptr(false),
|
||||||
}
|
}
|
||||||
|
|
||||||
attachment.URL = uris.URIForAttachment(
|
|
||||||
accountID,
|
|
||||||
string(TypeAttachment),
|
|
||||||
string(SizeOriginal),
|
|
||||||
attachment.ID,
|
|
||||||
"unknown",
|
|
||||||
)
|
|
||||||
|
|
||||||
attachment.File.Path = uris.StoragePathForAttachment(
|
|
||||||
accountID,
|
|
||||||
string(TypeAttachment),
|
|
||||||
string(SizeOriginal),
|
|
||||||
attachment.ID,
|
|
||||||
"unknown",
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check if we were provided additional info
|
// Check if we were provided additional info
|
||||||
// to add to the attachment, and overwrite
|
// to add to the attachment, and overwrite
|
||||||
// some of the attachment fields if so.
|
// some of the attachment fields if so.
|
||||||
if ai != nil {
|
if info.CreatedAt != nil {
|
||||||
if ai.CreatedAt != nil {
|
attachment.CreatedAt = *info.CreatedAt
|
||||||
attachment.CreatedAt = *ai.CreatedAt
|
}
|
||||||
|
if info.StatusID != nil {
|
||||||
|
attachment.StatusID = *info.StatusID
|
||||||
|
}
|
||||||
|
if info.RemoteURL != nil {
|
||||||
|
attachment.RemoteURL = *info.RemoteURL
|
||||||
|
}
|
||||||
|
if info.Description != nil {
|
||||||
|
attachment.Description = *info.Description
|
||||||
|
}
|
||||||
|
if info.ScheduledStatusID != nil {
|
||||||
|
attachment.ScheduledStatusID = *info.ScheduledStatusID
|
||||||
|
}
|
||||||
|
if info.Blurhash != nil {
|
||||||
|
attachment.Blurhash = *info.Blurhash
|
||||||
|
}
|
||||||
|
if info.Avatar != nil {
|
||||||
|
attachment.Avatar = info.Avatar
|
||||||
|
}
|
||||||
|
if info.Header != nil {
|
||||||
|
attachment.Header = info.Header
|
||||||
|
}
|
||||||
|
if info.FocusX != nil {
|
||||||
|
attachment.FileMeta.Focus.X = *info.FocusX
|
||||||
|
}
|
||||||
|
if info.FocusY != nil {
|
||||||
|
attachment.FileMeta.Focus.Y = *info.FocusY
|
||||||
}
|
}
|
||||||
|
|
||||||
if ai.StatusID != nil {
|
// Store attachment in database in initial form.
|
||||||
attachment.StatusID = *ai.StatusID
|
err := m.state.DB.PutAttachment(ctx, attachment)
|
||||||
}
|
|
||||||
|
|
||||||
if ai.RemoteURL != nil {
|
|
||||||
attachment.RemoteURL = *ai.RemoteURL
|
|
||||||
}
|
|
||||||
|
|
||||||
if ai.Description != nil {
|
|
||||||
attachment.Description = *ai.Description
|
|
||||||
}
|
|
||||||
|
|
||||||
if ai.ScheduledStatusID != nil {
|
|
||||||
attachment.ScheduledStatusID = *ai.ScheduledStatusID
|
|
||||||
}
|
|
||||||
|
|
||||||
if ai.Blurhash != nil {
|
|
||||||
attachment.Blurhash = *ai.Blurhash
|
|
||||||
}
|
|
||||||
|
|
||||||
if ai.Avatar != nil {
|
|
||||||
attachment.Avatar = ai.Avatar
|
|
||||||
}
|
|
||||||
|
|
||||||
if ai.Header != nil {
|
|
||||||
attachment.Header = ai.Header
|
|
||||||
}
|
|
||||||
|
|
||||||
if ai.FocusX != nil {
|
|
||||||
attachment.FileMeta.Focus.X = *ai.FocusX
|
|
||||||
}
|
|
||||||
|
|
||||||
if ai.FocusY != nil {
|
|
||||||
attachment.FileMeta.Focus.Y = *ai.FocusY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
processingMedia := &ProcessingMedia{
|
|
||||||
media: attachment,
|
|
||||||
dataFn: data,
|
|
||||||
mgr: m,
|
|
||||||
}
|
|
||||||
|
|
||||||
return processingMedia
|
|
||||||
}
|
|
||||||
|
|
||||||
// PreProcessMediaRecache refetches, reprocesses,
|
|
||||||
// and recaches an existing attachment that has
|
|
||||||
// been uncached via cleaner pruning.
|
|
||||||
//
|
|
||||||
// Note: unlike ProcessMedia, this will NOT queue
|
|
||||||
// the media to be asychronously processed.
|
|
||||||
func (m *Manager) PreProcessMediaRecache(
|
|
||||||
ctx context.Context,
|
|
||||||
data DataFunc,
|
|
||||||
attachmentID string,
|
|
||||||
) (*ProcessingMedia, error) {
|
|
||||||
// Get the existing attachment from database.
|
|
||||||
attachment, err := m.state.DB.GetAttachmentByID(ctx, attachmentID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
processingMedia := &ProcessingMedia{
|
// Pass prepared media as ready to be cached.
|
||||||
media: attachment,
|
return m.RecacheMedia(attachment, data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecacheMedia wraps a media model (assumed already
|
||||||
|
// inserted in the database!) with given data function
|
||||||
|
// to perform a blocking dereference / decode operation
|
||||||
|
// from the data stream returned.
|
||||||
|
func (m *Manager) RecacheMedia(
|
||||||
|
media *gtsmodel.MediaAttachment,
|
||||||
|
data DataFunc,
|
||||||
|
) *ProcessingMedia {
|
||||||
|
return &ProcessingMedia{
|
||||||
|
media: media,
|
||||||
dataFn: data,
|
dataFn: data,
|
||||||
recache: true, // Indicate it's a recache.
|
|
||||||
mgr: m,
|
mgr: m,
|
||||||
}
|
}
|
||||||
|
|
||||||
return processingMedia, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PreProcessEmoji begins the process of decoding and storing
|
// CreateEmoji creates a new emoji entry in the
|
||||||
// the given data as an emoji. It will return a pointer to a
|
// database for given shortcode, domain and extra
|
||||||
// ProcessingEmoji struct upon which further actions can be
|
// information, and prepares a new processing emoji
|
||||||
// performed, such as getting the finished media, thumbnail,
|
// entry to dereference it using the given data
|
||||||
// attachment, etc.
|
// function, decode the media and finish filling
|
||||||
//
|
// out remaining fields (e.g. type, path, etc).
|
||||||
// - data: function that the media manager can call
|
func (m *Manager) CreateEmoji(
|
||||||
// to return a reader containing the emoji data.
|
|
||||||
// - shortcode: the emoji shortcode without the ':'s around it.
|
|
||||||
// - emojiID: database ID that should be used to store the emoji.
|
|
||||||
// - uri: ActivityPub URI/ID of the emoji.
|
|
||||||
// - ai: optional and can be nil. Any additional information
|
|
||||||
// about the emoji provided will be put in the database.
|
|
||||||
// - refresh: refetch/refresh the emoji.
|
|
||||||
//
|
|
||||||
// Note: unlike ProcessEmoji, this will NOT queue
|
|
||||||
// the emoji to be asynchronously processed.
|
|
||||||
func (m *Manager) PreProcessEmoji(
|
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
data DataFunc,
|
|
||||||
shortcode string,
|
shortcode string,
|
||||||
emojiID string,
|
domain string,
|
||||||
uri string,
|
data DataFunc,
|
||||||
ai *AdditionalEmojiInfo,
|
info AdditionalEmojiInfo,
|
||||||
refresh bool,
|
) (
|
||||||
) (*ProcessingEmoji, error) {
|
*ProcessingEmoji,
|
||||||
var (
|
error,
|
||||||
newPathID string
|
) {
|
||||||
emoji *gtsmodel.Emoji
|
now := time.Now()
|
||||||
now = time.Now()
|
|
||||||
)
|
// Generate new ID.
|
||||||
|
id := id.NewULID()
|
||||||
|
|
||||||
// Fetch the local instance account for emoji path generation.
|
// Fetch the local instance account for emoji path generation.
|
||||||
instanceAcc, err := m.state.DB.GetInstanceAccount(ctx, "")
|
instanceAcc, err := m.state.DB.GetInstanceAccount(ctx, "")
|
||||||
|
@ -233,95 +229,31 @@ func (m *Manager) PreProcessEmoji(
|
||||||
return nil, gtserror.Newf("error fetching instance account: %w", err)
|
return nil, gtserror.Newf("error fetching instance account: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if refresh {
|
if domain == "" && info.URI == nil {
|
||||||
// Existing emoji!
|
// Generate URI for local emoji.
|
||||||
|
uri := uris.URIForEmoji(id)
|
||||||
emoji, err = m.state.DB.GetEmojiByID(ctx, emojiID)
|
info.URI = &uri
|
||||||
if err != nil {
|
|
||||||
err = gtserror.Newf("error fetching emoji to refresh from the db: %w", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since this is a refresh, we will end up with
|
// Generate static URL for attachment.
|
||||||
// new images stored for this emoji, so we should
|
staticURL := uris.URIForAttachment(
|
||||||
// use an io.Closer callback to perform clean up
|
|
||||||
// of the original images from storage.
|
|
||||||
originalData := data
|
|
||||||
originalImagePath := emoji.ImagePath
|
|
||||||
originalImageStaticPath := emoji.ImageStaticPath
|
|
||||||
|
|
||||||
data = func(ctx context.Context) (io.ReadCloser, int64, error) {
|
|
||||||
// Call original data func.
|
|
||||||
rc, sz, err := originalData(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap closer to cleanup old data.
|
|
||||||
c := iotools.CloserCallback(rc, func() {
|
|
||||||
if err := m.state.Storage.Delete(ctx, originalImagePath); err != nil && !storage.IsNotFound(err) {
|
|
||||||
log.Errorf(ctx, "error removing old emoji %s@%s from storage: %v", emoji.Shortcode, emoji.Domain, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.state.Storage.Delete(ctx, originalImageStaticPath); err != nil && !storage.IsNotFound(err) {
|
|
||||||
log.Errorf(ctx, "error removing old static emoji %s@%s from storage: %v", emoji.Shortcode, emoji.Domain, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Return newly wrapped readcloser and size.
|
|
||||||
return iotools.ReadCloser(rc, c), sz, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reuse existing shortcode and URI -
|
|
||||||
// these don't change when we refresh.
|
|
||||||
emoji.Shortcode = shortcode
|
|
||||||
emoji.URI = uri
|
|
||||||
|
|
||||||
// Use a new ID to create a new path
|
|
||||||
// for the new images, to get around
|
|
||||||
// needing to do cache invalidation.
|
|
||||||
newPathID, err = id.NewRandomULID()
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.Newf("error generating alternateID for emoji refresh: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
emoji.ImageStaticURL = uris.URIForAttachment(
|
|
||||||
instanceAcc.ID,
|
instanceAcc.ID,
|
||||||
string(TypeEmoji),
|
string(TypeEmoji),
|
||||||
string(SizeStatic),
|
string(SizeStatic),
|
||||||
newPathID,
|
id,
|
||||||
|
|
||||||
// All static emojis
|
// All static emojis
|
||||||
// are encoded as png.
|
// are encoded as png.
|
||||||
mimePng,
|
mimePng,
|
||||||
)
|
)
|
||||||
|
|
||||||
emoji.ImageStaticPath = uris.StoragePathForAttachment(
|
// Generate static image path for attachment.
|
||||||
|
staticPath := uris.StoragePathForAttachment(
|
||||||
instanceAcc.ID,
|
instanceAcc.ID,
|
||||||
string(TypeEmoji),
|
string(TypeEmoji),
|
||||||
string(SizeStatic),
|
string(SizeStatic),
|
||||||
newPathID,
|
id,
|
||||||
// All static emojis
|
|
||||||
// are encoded as png.
|
|
||||||
mimePng,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// New emoji!
|
|
||||||
|
|
||||||
imageStaticURL := uris.URIForAttachment(
|
|
||||||
instanceAcc.ID,
|
|
||||||
string(TypeEmoji),
|
|
||||||
string(SizeStatic),
|
|
||||||
emojiID,
|
|
||||||
// All static emojis
|
|
||||||
// are encoded as png.
|
|
||||||
mimePng,
|
|
||||||
)
|
|
||||||
|
|
||||||
imageStaticPath := uris.StoragePathForAttachment(
|
|
||||||
instanceAcc.ID,
|
|
||||||
string(TypeEmoji),
|
|
||||||
string(SizeStatic),
|
|
||||||
emojiID,
|
|
||||||
// All static emojis
|
// All static emojis
|
||||||
// are encoded as png.
|
// are encoded as png.
|
||||||
mimePng,
|
mimePng,
|
||||||
|
@ -330,57 +262,189 @@ func (m *Manager) PreProcessEmoji(
|
||||||
// Populate initial fields on the new emoji,
|
// Populate initial fields on the new emoji,
|
||||||
// leaving out fields with values we don't know
|
// leaving out fields with values we don't know
|
||||||
// yet. These will be overwritten as we go.
|
// yet. These will be overwritten as we go.
|
||||||
emoji = >smodel.Emoji{
|
emoji := >smodel.Emoji{
|
||||||
ID: emojiID,
|
ID: id,
|
||||||
|
Shortcode: shortcode,
|
||||||
|
Domain: domain,
|
||||||
|
ImageStaticURL: staticURL,
|
||||||
|
ImageStaticPath: staticPath,
|
||||||
|
ImageStaticContentType: mimeImagePng,
|
||||||
|
Disabled: util.Ptr(false),
|
||||||
|
VisibleInPicker: util.Ptr(true),
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
Shortcode: shortcode,
|
|
||||||
ImageStaticURL: imageStaticURL,
|
|
||||||
ImageStaticPath: imageStaticPath,
|
|
||||||
ImageStaticContentType: mimeImagePng,
|
|
||||||
ImageUpdatedAt: now,
|
|
||||||
Disabled: util.Ptr(false),
|
|
||||||
URI: uri,
|
|
||||||
VisibleInPicker: util.Ptr(true),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Finally, create new emoji.
|
||||||
|
return m.createEmoji(ctx,
|
||||||
|
m.state.DB.PutEmoji,
|
||||||
|
data,
|
||||||
|
emoji,
|
||||||
|
info,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshEmoji will prepare a recache operation
|
||||||
|
// for the given emoji, updating it with extra
|
||||||
|
// information, and in particular using new storage
|
||||||
|
// paths for the dereferenced media files to skirt
|
||||||
|
// around browser caching of the old files.
|
||||||
|
func (m *Manager) RefreshEmoji(
|
||||||
|
ctx context.Context,
|
||||||
|
emoji *gtsmodel.Emoji,
|
||||||
|
data DataFunc,
|
||||||
|
info AdditionalEmojiInfo,
|
||||||
|
) (
|
||||||
|
*ProcessingEmoji,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
// Fetch the local instance account for emoji path generation.
|
||||||
|
instanceAcc, err := m.state.DB.GetInstanceAccount(ctx, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.Newf("error fetching instance account: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create references to old emoji image
|
||||||
|
// paths before they get updated with new
|
||||||
|
// path ID. These are required for later
|
||||||
|
// deleting the old image files on refresh.
|
||||||
|
shortcodeDomain := util.ShortcodeDomain(emoji)
|
||||||
|
oldStaticPath := emoji.ImageStaticPath
|
||||||
|
oldPath := emoji.ImagePath
|
||||||
|
|
||||||
|
// Since this is a refresh we will end up storing new images at new
|
||||||
|
// paths, so we should wrap closer to delete old paths at completion.
|
||||||
|
wrapped := func(ctx context.Context) (io.ReadCloser, int64, error) {
|
||||||
|
|
||||||
|
// Call original data func.
|
||||||
|
rc, sz, err := data(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap closer to cleanup old data.
|
||||||
|
c := iotools.CloserFunc(func() error {
|
||||||
|
|
||||||
|
// First try close original.
|
||||||
|
if rc.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any *old* emoji image file path now stream is closed.
|
||||||
|
if err := m.state.Storage.Delete(ctx, oldPath); err != nil &&
|
||||||
|
!storage.IsNotFound(err) {
|
||||||
|
log.Errorf(ctx, "error deleting old emoji %s from storage: %v", shortcodeDomain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any *old* emoji static image file path now stream is closed.
|
||||||
|
if err := m.state.Storage.Delete(ctx, oldStaticPath); err != nil &&
|
||||||
|
!storage.IsNotFound(err) {
|
||||||
|
log.Errorf(ctx, "error deleting old static emoji %s from storage: %v", shortcodeDomain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Return newly wrapped readcloser and size.
|
||||||
|
return iotools.ReadCloser(rc, c), sz, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a new ID to create a new path
|
||||||
|
// for the new images, to get around
|
||||||
|
// needing to do cache invalidation.
|
||||||
|
newPathID, err := id.NewRandomULID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.Newf("error generating newPathID for emoji refresh: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new static URL for emoji.
|
||||||
|
emoji.ImageStaticURL = uris.URIForAttachment(
|
||||||
|
instanceAcc.ID,
|
||||||
|
string(TypeEmoji),
|
||||||
|
string(SizeStatic),
|
||||||
|
newPathID,
|
||||||
|
|
||||||
|
// All static emojis
|
||||||
|
// are encoded as png.
|
||||||
|
mimePng,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Generate new static image storage path for emoji.
|
||||||
|
emoji.ImageStaticPath = uris.StoragePathForAttachment(
|
||||||
|
instanceAcc.ID,
|
||||||
|
string(TypeEmoji),
|
||||||
|
string(SizeStatic),
|
||||||
|
newPathID,
|
||||||
|
|
||||||
|
// All static emojis
|
||||||
|
// are encoded as png.
|
||||||
|
mimePng,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Finally, create new emoji in database.
|
||||||
|
processingEmoji, err := m.createEmoji(ctx,
|
||||||
|
func(ctx context.Context, emoji *gtsmodel.Emoji) error {
|
||||||
|
return m.state.DB.UpdateEmoji(ctx, emoji)
|
||||||
|
},
|
||||||
|
wrapped,
|
||||||
|
emoji,
|
||||||
|
info,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the refreshed path ID used.
|
||||||
|
processingEmoji.newPathID = newPathID
|
||||||
|
|
||||||
|
return processingEmoji, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) createEmoji(
|
||||||
|
ctx context.Context,
|
||||||
|
putDB func(context.Context, *gtsmodel.Emoji) error,
|
||||||
|
data DataFunc,
|
||||||
|
emoji *gtsmodel.Emoji,
|
||||||
|
info AdditionalEmojiInfo,
|
||||||
|
) (
|
||||||
|
*ProcessingEmoji,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
// Check if we have additional info to add to the emoji,
|
// Check if we have additional info to add to the emoji,
|
||||||
// and overwrite some of the emoji fields if so.
|
// and overwrite some of the emoji fields if so.
|
||||||
if ai != nil {
|
if info.URI != nil {
|
||||||
if ai.CreatedAt != nil {
|
emoji.URI = *info.URI
|
||||||
emoji.CreatedAt = *ai.CreatedAt
|
}
|
||||||
|
if info.CreatedAt != nil {
|
||||||
|
emoji.CreatedAt = *info.CreatedAt
|
||||||
|
}
|
||||||
|
if info.Domain != nil {
|
||||||
|
emoji.Domain = *info.Domain
|
||||||
|
}
|
||||||
|
if info.ImageRemoteURL != nil {
|
||||||
|
emoji.ImageRemoteURL = *info.ImageRemoteURL
|
||||||
|
}
|
||||||
|
if info.ImageStaticRemoteURL != nil {
|
||||||
|
emoji.ImageStaticRemoteURL = *info.ImageStaticRemoteURL
|
||||||
|
}
|
||||||
|
if info.Disabled != nil {
|
||||||
|
emoji.Disabled = info.Disabled
|
||||||
|
}
|
||||||
|
if info.VisibleInPicker != nil {
|
||||||
|
emoji.VisibleInPicker = info.VisibleInPicker
|
||||||
|
}
|
||||||
|
if info.CategoryID != nil {
|
||||||
|
emoji.CategoryID = *info.CategoryID
|
||||||
}
|
}
|
||||||
|
|
||||||
if ai.Domain != nil {
|
// Store emoji in database in initial form.
|
||||||
emoji.Domain = *ai.Domain
|
if err := putDB(ctx, emoji); err != nil {
|
||||||
}
|
return nil, err
|
||||||
|
|
||||||
if ai.ImageRemoteURL != nil {
|
|
||||||
emoji.ImageRemoteURL = *ai.ImageRemoteURL
|
|
||||||
}
|
|
||||||
|
|
||||||
if ai.ImageStaticRemoteURL != nil {
|
|
||||||
emoji.ImageStaticRemoteURL = *ai.ImageStaticRemoteURL
|
|
||||||
}
|
|
||||||
|
|
||||||
if ai.Disabled != nil {
|
|
||||||
emoji.Disabled = ai.Disabled
|
|
||||||
}
|
|
||||||
|
|
||||||
if ai.VisibleInPicker != nil {
|
|
||||||
emoji.VisibleInPicker = ai.VisibleInPicker
|
|
||||||
}
|
|
||||||
|
|
||||||
if ai.CategoryID != nil {
|
|
||||||
emoji.CategoryID = *ai.CategoryID
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return wrapped emoji for later processing.
|
||||||
processingEmoji := &ProcessingEmoji{
|
processingEmoji := &ProcessingEmoji{
|
||||||
emoji: emoji,
|
emoji: emoji,
|
||||||
existing: refresh,
|
|
||||||
newPathID: newPathID,
|
|
||||||
dataFn: data,
|
dataFn: data,
|
||||||
mgr: m,
|
mgr: m,
|
||||||
}
|
}
|
||||||
|
@ -388,51 +452,17 @@ func (m *Manager) PreProcessEmoji(
|
||||||
return processingEmoji, nil
|
return processingEmoji, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PreProcessEmojiRecache refetches, reprocesses, and recaches
|
// RecacheEmoji wraps an emoji model (assumed already
|
||||||
// an existing emoji that has been uncached via cleaner pruning.
|
// inserted in the database!) with given data function
|
||||||
//
|
// to perform a blocking dereference / decode operation
|
||||||
// Note: unlike ProcessEmoji, this will NOT queue the emoji to
|
// from the data stream returned.
|
||||||
// be asychronously processed.
|
func (m *Manager) RecacheEmoji(
|
||||||
func (m *Manager) PreProcessEmojiRecache(
|
emoji *gtsmodel.Emoji,
|
||||||
ctx context.Context,
|
|
||||||
data DataFunc,
|
data DataFunc,
|
||||||
emojiID string,
|
) *ProcessingEmoji {
|
||||||
) (*ProcessingEmoji, error) {
|
return &ProcessingEmoji{
|
||||||
// Get the existing emoji from the database.
|
|
||||||
emoji, err := m.state.DB.GetEmojiByID(ctx, emojiID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
processingEmoji := &ProcessingEmoji{
|
|
||||||
emoji: emoji,
|
emoji: emoji,
|
||||||
dataFn: data,
|
dataFn: data,
|
||||||
existing: true, // Indicate recache.
|
|
||||||
mgr: m,
|
mgr: m,
|
||||||
}
|
}
|
||||||
|
|
||||||
return processingEmoji, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProcessEmoji will call PreProcessEmoji, followed
|
|
||||||
// by queuing the emoji in the emoji worker queue.
|
|
||||||
func (m *Manager) ProcessEmoji(
|
|
||||||
ctx context.Context,
|
|
||||||
data DataFunc,
|
|
||||||
shortcode string,
|
|
||||||
id string,
|
|
||||||
uri string,
|
|
||||||
ai *AdditionalEmojiInfo,
|
|
||||||
refresh bool,
|
|
||||||
) (*ProcessingEmoji, error) {
|
|
||||||
// Create a new processing emoji object for this emoji request.
|
|
||||||
emoji, err := m.PreProcessEmoji(ctx, data, shortcode, id, uri, ai, refresh)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to add emoji item to the worker queue.
|
|
||||||
m.state.Workers.Media.Queue.Push(emoji.Process)
|
|
||||||
|
|
||||||
return emoji, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ type ManagerTestSuite struct {
|
||||||
MediaStandardTestSuite
|
MediaStandardTestSuite
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ManagerTestSuite) TestEmojiProcessBlocking() {
|
func (suite *ManagerTestSuite) TestEmojiProcess() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
||||||
|
@ -52,27 +52,26 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlocking() {
|
||||||
return io.NopCloser(bytes.NewBuffer(b)), int64(len(b)), nil
|
return io.NopCloser(bytes.NewBuffer(b)), int64(len(b)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
emojiID := "01GDQ9G782X42BAMFASKP64343"
|
processing, err := suite.manager.CreateEmoji(ctx,
|
||||||
emojiURI := "http://localhost:8080/emoji/01GDQ9G782X42BAMFASKP64343"
|
"rainbow_test",
|
||||||
|
"",
|
||||||
processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, "rainbow_test", emojiID, emojiURI, nil, false)
|
data,
|
||||||
|
media.AdditionalEmojiInfo{},
|
||||||
|
)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
// do a blocking call to fetch the emoji
|
// do a blocking call to fetch the emoji
|
||||||
emoji, err := processingEmoji.LoadEmoji(ctx)
|
emoji, err := processing.Load(ctx)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(emoji)
|
suite.NotNil(emoji)
|
||||||
|
|
||||||
// make sure it's got the stuff set on it that we expect
|
|
||||||
suite.Equal(emojiID, emoji.ID)
|
|
||||||
|
|
||||||
// file meta should be correctly derived from the image
|
// file meta should be correctly derived from the image
|
||||||
suite.Equal("image/png", emoji.ImageContentType)
|
suite.Equal("image/png", emoji.ImageContentType)
|
||||||
suite.Equal("image/png", emoji.ImageStaticContentType)
|
suite.Equal("image/png", emoji.ImageStaticContentType)
|
||||||
suite.Equal(36702, emoji.ImageFileSize)
|
suite.Equal(36702, emoji.ImageFileSize)
|
||||||
|
|
||||||
// now make sure the emoji is in the database
|
// now make sure the emoji is in the database
|
||||||
dbEmoji, err := suite.db.GetEmojiByID(ctx, emojiID)
|
dbEmoji, err := suite.db.GetEmojiByID(ctx, emoji.ID)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(dbEmoji)
|
suite.NotNil(dbEmoji)
|
||||||
|
|
||||||
|
@ -101,14 +100,15 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlocking() {
|
||||||
suite.Equal(processedStaticBytesExpected, processedStaticBytes)
|
suite.Equal(processedStaticBytesExpected, processedStaticBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ManagerTestSuite) TestEmojiProcessBlockingRefresh() {
|
func (suite *ManagerTestSuite) TestEmojiProcessRefresh() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// we're going to 'refresh' the remote 'yell' emoji by changing the image url to the pixellated gts logo
|
// we're going to 'refresh' the remote 'yell' emoji by changing the image url to the pixellated gts logo
|
||||||
originalEmoji := suite.testEmojis["yell"]
|
originalEmoji := suite.testEmojis["yell"]
|
||||||
|
|
||||||
emojiToUpdate := >smodel.Emoji{}
|
emojiToUpdate, err := suite.db.GetEmojiByID(ctx, originalEmoji.ID)
|
||||||
*emojiToUpdate = *originalEmoji
|
suite.NoError(err)
|
||||||
|
|
||||||
newImageRemoteURL := "http://fossbros-anonymous.io/some/image/path.png"
|
newImageRemoteURL := "http://fossbros-anonymous.io/some/image/path.png"
|
||||||
|
|
||||||
oldEmojiImagePath := emojiToUpdate.ImagePath
|
oldEmojiImagePath := emojiToUpdate.ImagePath
|
||||||
|
@ -122,23 +122,24 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlockingRefresh() {
|
||||||
return io.NopCloser(bytes.NewBuffer(b)), int64(len(b)), nil
|
return io.NopCloser(bytes.NewBuffer(b)), int64(len(b)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
emojiID := emojiToUpdate.ID
|
processing, err := suite.manager.RefreshEmoji(ctx,
|
||||||
emojiURI := emojiToUpdate.URI
|
emojiToUpdate,
|
||||||
|
data,
|
||||||
processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, "yell", emojiID, emojiURI, &media.AdditionalEmojiInfo{
|
media.AdditionalEmojiInfo{
|
||||||
CreatedAt: &emojiToUpdate.CreatedAt,
|
CreatedAt: &emojiToUpdate.CreatedAt,
|
||||||
Domain: &emojiToUpdate.Domain,
|
Domain: &emojiToUpdate.Domain,
|
||||||
ImageRemoteURL: &newImageRemoteURL,
|
ImageRemoteURL: &newImageRemoteURL,
|
||||||
}, true)
|
},
|
||||||
|
)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
// do a blocking call to fetch the emoji
|
// do a blocking call to fetch the emoji
|
||||||
emoji, err := processingEmoji.LoadEmoji(ctx)
|
emoji, err := processing.Load(ctx)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(emoji)
|
suite.NotNil(emoji)
|
||||||
|
|
||||||
// make sure it's got the stuff set on it that we expect
|
// make sure it's got the stuff set on it that we expect
|
||||||
suite.Equal(emojiID, emoji.ID)
|
suite.Equal(originalEmoji.ID, emoji.ID)
|
||||||
|
|
||||||
// file meta should be correctly derived from the image
|
// file meta should be correctly derived from the image
|
||||||
suite.Equal("image/png", emoji.ImageContentType)
|
suite.Equal("image/png", emoji.ImageContentType)
|
||||||
|
@ -146,7 +147,7 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlockingRefresh() {
|
||||||
suite.Equal(10296, emoji.ImageFileSize)
|
suite.Equal(10296, emoji.ImageFileSize)
|
||||||
|
|
||||||
// now make sure the emoji is in the database
|
// now make sure the emoji is in the database
|
||||||
dbEmoji, err := suite.db.GetEmojiByID(ctx, emojiID)
|
dbEmoji, err := suite.db.GetEmojiByID(ctx, emoji.ID)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(dbEmoji)
|
suite.NotNil(dbEmoji)
|
||||||
|
|
||||||
|
@ -185,7 +186,6 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlockingRefresh() {
|
||||||
suite.NotEqual(originalEmoji.ImageStaticPath, dbEmoji.ImageStaticPath)
|
suite.NotEqual(originalEmoji.ImageStaticPath, dbEmoji.ImageStaticPath)
|
||||||
suite.NotEqual(originalEmoji.ImageStaticPath, dbEmoji.ImageStaticPath)
|
suite.NotEqual(originalEmoji.ImageStaticPath, dbEmoji.ImageStaticPath)
|
||||||
suite.NotEqual(originalEmoji.UpdatedAt, dbEmoji.UpdatedAt)
|
suite.NotEqual(originalEmoji.UpdatedAt, dbEmoji.UpdatedAt)
|
||||||
suite.NotEqual(originalEmoji.ImageUpdatedAt, dbEmoji.ImageUpdatedAt)
|
|
||||||
|
|
||||||
// the old image files should no longer be in storage
|
// the old image files should no longer be in storage
|
||||||
_, err = suite.storage.Get(ctx, oldEmojiImagePath)
|
_, err = suite.storage.Get(ctx, oldEmojiImagePath)
|
||||||
|
@ -194,7 +194,7 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlockingRefresh() {
|
||||||
suite.True(storage.IsNotFound(err))
|
suite.True(storage.IsNotFound(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ManagerTestSuite) TestEmojiProcessBlockingTooLarge() {
|
func (suite *ManagerTestSuite) TestEmojiProcessTooLarge() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
||||||
|
@ -206,19 +206,20 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlockingTooLarge() {
|
||||||
return io.NopCloser(bytes.NewBuffer(b)), int64(len(b)), nil
|
return io.NopCloser(bytes.NewBuffer(b)), int64(len(b)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
emojiID := "01GDQ9G782X42BAMFASKP64343"
|
processing, err := suite.manager.CreateEmoji(ctx,
|
||||||
emojiURI := "http://localhost:8080/emoji/01GDQ9G782X42BAMFASKP64343"
|
"big_panda",
|
||||||
|
"",
|
||||||
processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, "big_panda", emojiID, emojiURI, nil, false)
|
data,
|
||||||
|
media.AdditionalEmojiInfo{},
|
||||||
|
)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
// do a blocking call to fetch the emoji
|
// do a blocking call to fetch the emoji
|
||||||
emoji, err := processingEmoji.LoadEmoji(ctx)
|
_, err = processing.Load(ctx)
|
||||||
suite.EqualError(err, "store: given emoji size 630kiB greater than max allowed 50.0kiB")
|
suite.EqualError(err, "store: given emoji size 630kiB greater than max allowed 50.0kiB")
|
||||||
suite.Nil(emoji)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ManagerTestSuite) TestEmojiProcessBlockingTooLargeNoSizeGiven() {
|
func (suite *ManagerTestSuite) TestEmojiProcessTooLargeNoSizeGiven() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
||||||
|
@ -230,19 +231,20 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlockingTooLargeNoSizeGiven() {
|
||||||
return io.NopCloser(bytes.NewBuffer(b)), -1, nil
|
return io.NopCloser(bytes.NewBuffer(b)), -1, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
emojiID := "01GDQ9G782X42BAMFASKP64343"
|
processing, err := suite.manager.CreateEmoji(ctx,
|
||||||
emojiURI := "http://localhost:8080/emoji/01GDQ9G782X42BAMFASKP64343"
|
"big_panda",
|
||||||
|
"",
|
||||||
processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, "big_panda", emojiID, emojiURI, nil, false)
|
data,
|
||||||
|
media.AdditionalEmojiInfo{},
|
||||||
|
)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
// do a blocking call to fetch the emoji
|
// do a blocking call to fetch the emoji
|
||||||
emoji, err := processingEmoji.LoadEmoji(ctx)
|
_, err = processing.Load(ctx)
|
||||||
suite.EqualError(err, "store: calculated emoji size 630kiB greater than max allowed 50.0kiB")
|
suite.EqualError(err, "store: written emoji size 630kiB greater than max allowed 50.0kiB")
|
||||||
suite.Nil(emoji)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ManagerTestSuite) TestEmojiProcessBlockingNoFileSizeGiven() {
|
func (suite *ManagerTestSuite) TestEmojiProcessNoFileSizeGiven() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
||||||
|
@ -254,28 +256,27 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlockingNoFileSizeGiven() {
|
||||||
return io.NopCloser(bytes.NewBuffer(b)), -1, nil
|
return io.NopCloser(bytes.NewBuffer(b)), -1, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
emojiID := "01GDQ9G782X42BAMFASKP64343"
|
|
||||||
emojiURI := "http://localhost:8080/emoji/01GDQ9G782X42BAMFASKP64343"
|
|
||||||
|
|
||||||
// process the media with no additional info provided
|
// process the media with no additional info provided
|
||||||
processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, "rainbow_test", emojiID, emojiURI, nil, false)
|
processing, err := suite.manager.CreateEmoji(ctx,
|
||||||
|
"rainbow_test",
|
||||||
|
"",
|
||||||
|
data,
|
||||||
|
media.AdditionalEmojiInfo{},
|
||||||
|
)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
// do a blocking call to fetch the emoji
|
// do a blocking call to fetch the emoji
|
||||||
emoji, err := processingEmoji.LoadEmoji(ctx)
|
emoji, err := processing.Load(ctx)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(emoji)
|
suite.NotNil(emoji)
|
||||||
|
|
||||||
// make sure it's got the stuff set on it that we expect
|
|
||||||
suite.Equal(emojiID, emoji.ID)
|
|
||||||
|
|
||||||
// file meta should be correctly derived from the image
|
// file meta should be correctly derived from the image
|
||||||
suite.Equal("image/png", emoji.ImageContentType)
|
suite.Equal("image/png", emoji.ImageContentType)
|
||||||
suite.Equal("image/png", emoji.ImageStaticContentType)
|
suite.Equal("image/png", emoji.ImageStaticContentType)
|
||||||
suite.Equal(36702, emoji.ImageFileSize)
|
suite.Equal(36702, emoji.ImageFileSize)
|
||||||
|
|
||||||
// now make sure the emoji is in the database
|
// now make sure the emoji is in the database
|
||||||
dbEmoji, err := suite.db.GetEmojiByID(ctx, emojiID)
|
dbEmoji, err := suite.db.GetEmojiByID(ctx, emoji.ID)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(dbEmoji)
|
suite.NotNil(dbEmoji)
|
||||||
|
|
||||||
|
@ -316,27 +317,27 @@ func (suite *ManagerTestSuite) TestEmojiWebpProcess() {
|
||||||
return io.NopCloser(bytes.NewBuffer(b)), int64(len(b)), nil
|
return io.NopCloser(bytes.NewBuffer(b)), int64(len(b)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
emojiID := "01GDQ9G782X42BAMFASKP64343"
|
// process the media with no additional info provided
|
||||||
emojiURI := "http://localhost:8080/emoji/01GDQ9G782X42BAMFASKP64343"
|
processing, err := suite.manager.CreateEmoji(ctx,
|
||||||
|
"nb-flag",
|
||||||
processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, "nb-flag", emojiID, emojiURI, nil, false)
|
"",
|
||||||
|
data,
|
||||||
|
media.AdditionalEmojiInfo{},
|
||||||
|
)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
// do a blocking call to fetch the emoji
|
// do a blocking call to fetch the emoji
|
||||||
emoji, err := processingEmoji.LoadEmoji(ctx)
|
emoji, err := processing.Load(ctx)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(emoji)
|
suite.NotNil(emoji)
|
||||||
|
|
||||||
// make sure it's got the stuff set on it that we expect
|
|
||||||
suite.Equal(emojiID, emoji.ID)
|
|
||||||
|
|
||||||
// file meta should be correctly derived from the image
|
// file meta should be correctly derived from the image
|
||||||
suite.Equal("image/webp", emoji.ImageContentType)
|
suite.Equal("image/webp", emoji.ImageContentType)
|
||||||
suite.Equal("image/png", emoji.ImageStaticContentType)
|
suite.Equal("image/png", emoji.ImageStaticContentType)
|
||||||
suite.Equal(294, emoji.ImageFileSize)
|
suite.Equal(294, emoji.ImageFileSize)
|
||||||
|
|
||||||
// now make sure the emoji is in the database
|
// now make sure the emoji is in the database
|
||||||
dbEmoji, err := suite.db.GetEmojiByID(ctx, emojiID)
|
dbEmoji, err := suite.db.GetEmojiByID(ctx, emoji.ID)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(dbEmoji)
|
suite.NotNil(dbEmoji)
|
||||||
|
|
||||||
|
@ -365,7 +366,7 @@ func (suite *ManagerTestSuite) TestEmojiWebpProcess() {
|
||||||
suite.Equal(processedStaticBytesExpected, processedStaticBytes)
|
suite.Equal(processedStaticBytesExpected, processedStaticBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ManagerTestSuite) TestSimpleJpegProcessBlocking() {
|
func (suite *ManagerTestSuite) TestSimpleJpegProcess() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
||||||
|
@ -380,18 +381,22 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlocking() {
|
||||||
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
|
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
|
||||||
|
|
||||||
// process the media with no additional info provided
|
// process the media with no additional info provided
|
||||||
processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
|
processing, err := suite.manager.CreateMedia(ctx,
|
||||||
// fetch the attachment id from the processing media
|
accountID,
|
||||||
attachmentID := processingMedia.AttachmentID()
|
data,
|
||||||
|
media.AdditionalMediaInfo{},
|
||||||
|
)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(processing)
|
||||||
|
|
||||||
// do a blocking call to fetch the attachment
|
// do a blocking call to fetch the attachment
|
||||||
attachment, err := processingMedia.LoadAttachment(ctx)
|
attachment, err := processing.Load(ctx)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(attachment)
|
suite.NotNil(attachment)
|
||||||
|
|
||||||
// make sure it's got the stuff set on it that we expect
|
// make sure it's got the stuff set on it that we expect
|
||||||
// the attachment ID and accountID we expect
|
// the attachment ID and accountID we expect
|
||||||
suite.Equal(attachmentID, attachment.ID)
|
suite.Equal(processing.ID(), attachment.ID)
|
||||||
suite.Equal(accountID, attachment.AccountID)
|
suite.Equal(accountID, attachment.AccountID)
|
||||||
|
|
||||||
// file meta should be correctly derived from the image
|
// file meta should be correctly derived from the image
|
||||||
|
@ -407,7 +412,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlocking() {
|
||||||
suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
|
suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
|
||||||
|
|
||||||
// now make sure the attachment is in the database
|
// now make sure the attachment is in the database
|
||||||
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
|
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(dbAttachment)
|
suite.NotNil(dbAttachment)
|
||||||
|
|
||||||
|
@ -456,13 +461,16 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessPartial() {
|
||||||
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
|
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
|
||||||
|
|
||||||
// process the media with no additional info provided
|
// process the media with no additional info provided
|
||||||
processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
|
processing, err := suite.manager.CreateMedia(ctx,
|
||||||
|
accountID,
|
||||||
// fetch the attachment id from the processing media
|
data,
|
||||||
attachmentID := processingMedia.AttachmentID()
|
media.AdditionalMediaInfo{},
|
||||||
|
)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(processing)
|
||||||
|
|
||||||
// do a blocking call to fetch the attachment
|
// do a blocking call to fetch the attachment
|
||||||
attachment, err := processingMedia.LoadAttachment(ctx)
|
attachment, err := processing.Load(ctx)
|
||||||
|
|
||||||
// Since we're cutting off the byte stream
|
// Since we're cutting off the byte stream
|
||||||
// halfway through, we should get an error here.
|
// halfway through, we should get an error here.
|
||||||
|
@ -471,17 +479,16 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessPartial() {
|
||||||
|
|
||||||
// make sure it's got the stuff set on it that we expect
|
// make sure it's got the stuff set on it that we expect
|
||||||
// the attachment ID and accountID we expect
|
// the attachment ID and accountID we expect
|
||||||
suite.Equal(attachmentID, attachment.ID)
|
suite.Equal(processing.ID(), attachment.ID)
|
||||||
suite.Equal(accountID, attachment.AccountID)
|
suite.Equal(accountID, attachment.AccountID)
|
||||||
|
|
||||||
// file meta should be correctly derived from the image
|
// file meta should be correctly derived from the image
|
||||||
suite.Zero(attachment.FileMeta)
|
suite.Zero(attachment.FileMeta)
|
||||||
suite.Equal("image/jpeg", attachment.File.ContentType)
|
suite.Equal("image/jpeg", attachment.File.ContentType)
|
||||||
suite.Equal("image/jpeg", attachment.Thumbnail.ContentType)
|
|
||||||
suite.Empty(attachment.Blurhash)
|
suite.Empty(attachment.Blurhash)
|
||||||
|
|
||||||
// now make sure the attachment is in the database
|
// now make sure the attachment is in the database
|
||||||
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
|
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(dbAttachment)
|
suite.NotNil(dbAttachment)
|
||||||
|
|
||||||
|
@ -518,19 +525,22 @@ func (suite *ManagerTestSuite) TestPDFProcess() {
|
||||||
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
|
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
|
||||||
|
|
||||||
// process the media with no additional info provided
|
// process the media with no additional info provided
|
||||||
processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
|
processing, err := suite.manager.CreateMedia(ctx,
|
||||||
|
accountID,
|
||||||
// fetch the attachment id from the processing media
|
data,
|
||||||
attachmentID := processingMedia.AttachmentID()
|
media.AdditionalMediaInfo{},
|
||||||
|
)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(processing)
|
||||||
|
|
||||||
// do a blocking call to fetch the attachment
|
// do a blocking call to fetch the attachment
|
||||||
attachment, err := processingMedia.LoadAttachment(ctx)
|
attachment, err := processing.Load(ctx)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(attachment)
|
suite.NotNil(attachment)
|
||||||
|
|
||||||
// make sure it's got the stuff set on it that we expect
|
// make sure it's got the stuff set on it that we expect
|
||||||
// the attachment ID and accountID we expect
|
// the attachment ID and accountID we expect
|
||||||
suite.Equal(attachmentID, attachment.ID)
|
suite.Equal(processing.ID(), attachment.ID)
|
||||||
suite.Equal(accountID, attachment.AccountID)
|
suite.Equal(accountID, attachment.AccountID)
|
||||||
|
|
||||||
// file meta should be correctly derived from the image
|
// file meta should be correctly derived from the image
|
||||||
|
@ -540,7 +550,7 @@ func (suite *ManagerTestSuite) TestPDFProcess() {
|
||||||
suite.Empty(attachment.Blurhash)
|
suite.Empty(attachment.Blurhash)
|
||||||
|
|
||||||
// now make sure the attachment is in the database
|
// now make sure the attachment is in the database
|
||||||
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
|
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(dbAttachment)
|
suite.NotNil(dbAttachment)
|
||||||
|
|
||||||
|
@ -561,7 +571,7 @@ func (suite *ManagerTestSuite) TestPDFProcess() {
|
||||||
suite.False(stored)
|
suite.False(stored)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ManagerTestSuite) TestSlothVineProcessBlocking() {
|
func (suite *ManagerTestSuite) TestSlothVineProcess() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
||||||
|
@ -576,18 +586,22 @@ func (suite *ManagerTestSuite) TestSlothVineProcessBlocking() {
|
||||||
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
|
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
|
||||||
|
|
||||||
// process the media with no additional info provided
|
// process the media with no additional info provided
|
||||||
processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
|
processing, err := suite.manager.CreateMedia(ctx,
|
||||||
// fetch the attachment id from the processing media
|
accountID,
|
||||||
attachmentID := processingMedia.AttachmentID()
|
data,
|
||||||
|
media.AdditionalMediaInfo{},
|
||||||
|
)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(processing)
|
||||||
|
|
||||||
// do a blocking call to fetch the attachment
|
// do a blocking call to fetch the attachment
|
||||||
attachment, err := processingMedia.LoadAttachment(ctx)
|
attachment, err := processing.Load(ctx)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(attachment)
|
suite.NotNil(attachment)
|
||||||
|
|
||||||
// make sure it's got the stuff set on it that we expect
|
// make sure it's got the stuff set on it that we expect
|
||||||
// the attachment ID and accountID we expect
|
// the attachment ID and accountID we expect
|
||||||
suite.Equal(attachmentID, attachment.ID)
|
suite.Equal(processing.ID(), attachment.ID)
|
||||||
suite.Equal(accountID, attachment.AccountID)
|
suite.Equal(accountID, attachment.AccountID)
|
||||||
|
|
||||||
// file meta should be correctly derived from the video
|
// file meta should be correctly derived from the video
|
||||||
|
@ -607,7 +621,7 @@ func (suite *ManagerTestSuite) TestSlothVineProcessBlocking() {
|
||||||
suite.Equal("L00000fQfQfQfQfQfQfQfQfQfQfQ", attachment.Blurhash)
|
suite.Equal("L00000fQfQfQfQfQfQfQfQfQfQfQ", attachment.Blurhash)
|
||||||
|
|
||||||
// now make sure the attachment is in the database
|
// now make sure the attachment is in the database
|
||||||
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
|
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(dbAttachment)
|
suite.NotNil(dbAttachment)
|
||||||
|
|
||||||
|
@ -636,7 +650,7 @@ func (suite *ManagerTestSuite) TestSlothVineProcessBlocking() {
|
||||||
suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
|
suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ManagerTestSuite) TestLongerMp4ProcessBlocking() {
|
func (suite *ManagerTestSuite) TestLongerMp4Process() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
||||||
|
@ -651,18 +665,22 @@ func (suite *ManagerTestSuite) TestLongerMp4ProcessBlocking() {
|
||||||
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
|
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
|
||||||
|
|
||||||
// process the media with no additional info provided
|
// process the media with no additional info provided
|
||||||
processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
|
processing, err := suite.manager.CreateMedia(ctx,
|
||||||
// fetch the attachment id from the processing media
|
accountID,
|
||||||
attachmentID := processingMedia.AttachmentID()
|
data,
|
||||||
|
media.AdditionalMediaInfo{},
|
||||||
|
)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(processing)
|
||||||
|
|
||||||
// do a blocking call to fetch the attachment
|
// do a blocking call to fetch the attachment
|
||||||
attachment, err := processingMedia.LoadAttachment(ctx)
|
attachment, err := processing.Load(ctx)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(attachment)
|
suite.NotNil(attachment)
|
||||||
|
|
||||||
// make sure it's got the stuff set on it that we expect
|
// make sure it's got the stuff set on it that we expect
|
||||||
// the attachment ID and accountID we expect
|
// the attachment ID and accountID we expect
|
||||||
suite.Equal(attachmentID, attachment.ID)
|
suite.Equal(processing.ID(), attachment.ID)
|
||||||
suite.Equal(accountID, attachment.AccountID)
|
suite.Equal(accountID, attachment.AccountID)
|
||||||
|
|
||||||
// file meta should be correctly derived from the video
|
// file meta should be correctly derived from the video
|
||||||
|
@ -682,7 +700,7 @@ func (suite *ManagerTestSuite) TestLongerMp4ProcessBlocking() {
|
||||||
suite.Equal("L00000fQfQfQfQfQfQfQfQfQfQfQ", attachment.Blurhash)
|
suite.Equal("L00000fQfQfQfQfQfQfQfQfQfQfQ", attachment.Blurhash)
|
||||||
|
|
||||||
// now make sure the attachment is in the database
|
// now make sure the attachment is in the database
|
||||||
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
|
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(dbAttachment)
|
suite.NotNil(dbAttachment)
|
||||||
|
|
||||||
|
@ -711,7 +729,7 @@ func (suite *ManagerTestSuite) TestLongerMp4ProcessBlocking() {
|
||||||
suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
|
suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ManagerTestSuite) TestBirdnestMp4ProcessBlocking() {
|
func (suite *ManagerTestSuite) TestBirdnestMp4Process() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
||||||
|
@ -726,18 +744,22 @@ func (suite *ManagerTestSuite) TestBirdnestMp4ProcessBlocking() {
|
||||||
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
|
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
|
||||||
|
|
||||||
// process the media with no additional info provided
|
// process the media with no additional info provided
|
||||||
processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
|
processing, err := suite.manager.CreateMedia(ctx,
|
||||||
// fetch the attachment id from the processing media
|
accountID,
|
||||||
attachmentID := processingMedia.AttachmentID()
|
data,
|
||||||
|
media.AdditionalMediaInfo{},
|
||||||
|
)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(processing)
|
||||||
|
|
||||||
// do a blocking call to fetch the attachment
|
// do a blocking call to fetch the attachment
|
||||||
attachment, err := processingMedia.LoadAttachment(ctx)
|
attachment, err := processing.Load(ctx)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(attachment)
|
suite.NotNil(attachment)
|
||||||
|
|
||||||
// make sure it's got the stuff set on it that we expect
|
// make sure it's got the stuff set on it that we expect
|
||||||
// the attachment ID and accountID we expect
|
// the attachment ID and accountID we expect
|
||||||
suite.Equal(attachmentID, attachment.ID)
|
suite.Equal(processing.ID(), attachment.ID)
|
||||||
suite.Equal(accountID, attachment.AccountID)
|
suite.Equal(accountID, attachment.AccountID)
|
||||||
|
|
||||||
// file meta should be correctly derived from the video
|
// file meta should be correctly derived from the video
|
||||||
|
@ -757,7 +779,7 @@ func (suite *ManagerTestSuite) TestBirdnestMp4ProcessBlocking() {
|
||||||
suite.Equal("L00000fQfQfQfQfQfQfQfQfQfQfQ", attachment.Blurhash)
|
suite.Equal("L00000fQfQfQfQfQfQfQfQfQfQfQ", attachment.Blurhash)
|
||||||
|
|
||||||
// now make sure the attachment is in the database
|
// now make sure the attachment is in the database
|
||||||
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
|
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(dbAttachment)
|
suite.NotNil(dbAttachment)
|
||||||
|
|
||||||
|
@ -786,7 +808,7 @@ func (suite *ManagerTestSuite) TestBirdnestMp4ProcessBlocking() {
|
||||||
suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
|
suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ManagerTestSuite) TestNotAnMp4ProcessBlocking() {
|
func (suite *ManagerTestSuite) TestNotAnMp4Process() {
|
||||||
// try to load an 'mp4' that's actually an mkv in disguise
|
// try to load an 'mp4' that's actually an mkv in disguise
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
@ -803,10 +825,16 @@ func (suite *ManagerTestSuite) TestNotAnMp4ProcessBlocking() {
|
||||||
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
|
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
|
||||||
|
|
||||||
// pre processing should go fine but...
|
// pre processing should go fine but...
|
||||||
processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
|
processing, err := suite.manager.CreateMedia(ctx,
|
||||||
|
accountID,
|
||||||
|
data,
|
||||||
|
media.AdditionalMediaInfo{},
|
||||||
|
)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(processing)
|
||||||
|
|
||||||
// we should get an error while loading
|
// we should get an error while loading
|
||||||
attachment, err := processingMedia.LoadAttachment(ctx)
|
attachment, err := processing.Load(ctx)
|
||||||
suite.EqualError(err, "finish: error decoding video: error determining video metadata: [width height framerate]")
|
suite.EqualError(err, "finish: error decoding video: error determining video metadata: [width height framerate]")
|
||||||
|
|
||||||
// partial attachment should be
|
// partial attachment should be
|
||||||
|
@ -815,7 +843,7 @@ func (suite *ManagerTestSuite) TestNotAnMp4ProcessBlocking() {
|
||||||
suite.Equal(gtsmodel.FileTypeUnknown, attachment.Type)
|
suite.Equal(gtsmodel.FileTypeUnknown, attachment.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingNoContentLengthGiven() {
|
func (suite *ManagerTestSuite) TestSimpleJpegProcessNoContentLengthGiven() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
||||||
|
@ -831,18 +859,22 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingNoContentLengthGiven
|
||||||
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
|
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
|
||||||
|
|
||||||
// process the media with no additional info provided
|
// process the media with no additional info provided
|
||||||
processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
|
processing, err := suite.manager.CreateMedia(ctx,
|
||||||
// fetch the attachment id from the processing media
|
accountID,
|
||||||
attachmentID := processingMedia.AttachmentID()
|
data,
|
||||||
|
media.AdditionalMediaInfo{},
|
||||||
|
)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(processing)
|
||||||
|
|
||||||
// do a blocking call to fetch the attachment
|
// do a blocking call to fetch the attachment
|
||||||
attachment, err := processingMedia.LoadAttachment(ctx)
|
attachment, err := processing.Load(ctx)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(attachment)
|
suite.NotNil(attachment)
|
||||||
|
|
||||||
// make sure it's got the stuff set on it that we expect
|
// make sure it's got the stuff set on it that we expect
|
||||||
// the attachment ID and accountID we expect
|
// the attachment ID and accountID we expect
|
||||||
suite.Equal(attachmentID, attachment.ID)
|
suite.Equal(processing.ID(), attachment.ID)
|
||||||
suite.Equal(accountID, attachment.AccountID)
|
suite.Equal(accountID, attachment.AccountID)
|
||||||
|
|
||||||
// file meta should be correctly derived from the image
|
// file meta should be correctly derived from the image
|
||||||
|
@ -858,7 +890,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingNoContentLengthGiven
|
||||||
suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
|
suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
|
||||||
|
|
||||||
// now make sure the attachment is in the database
|
// now make sure the attachment is in the database
|
||||||
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
|
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(dbAttachment)
|
suite.NotNil(dbAttachment)
|
||||||
|
|
||||||
|
@ -887,7 +919,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingNoContentLengthGiven
|
||||||
suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
|
suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingReadCloser() {
|
func (suite *ManagerTestSuite) TestSimpleJpegProcessReadCloser() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
||||||
|
@ -903,18 +935,22 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingReadCloser() {
|
||||||
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
|
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
|
||||||
|
|
||||||
// process the media with no additional info provided
|
// process the media with no additional info provided
|
||||||
processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
|
processing, err := suite.manager.CreateMedia(ctx,
|
||||||
// fetch the attachment id from the processing media
|
accountID,
|
||||||
attachmentID := processingMedia.AttachmentID()
|
data,
|
||||||
|
media.AdditionalMediaInfo{},
|
||||||
|
)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(processing)
|
||||||
|
|
||||||
// do a blocking call to fetch the attachment
|
// do a blocking call to fetch the attachment
|
||||||
attachment, err := processingMedia.LoadAttachment(ctx)
|
attachment, err := processing.Load(ctx)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(attachment)
|
suite.NotNil(attachment)
|
||||||
|
|
||||||
// make sure it's got the stuff set on it that we expect
|
// make sure it's got the stuff set on it that we expect
|
||||||
// the attachment ID and accountID we expect
|
// the attachment ID and accountID we expect
|
||||||
suite.Equal(attachmentID, attachment.ID)
|
suite.Equal(processing.ID(), attachment.ID)
|
||||||
suite.Equal(accountID, attachment.AccountID)
|
suite.Equal(accountID, attachment.AccountID)
|
||||||
|
|
||||||
// file meta should be correctly derived from the image
|
// file meta should be correctly derived from the image
|
||||||
|
@ -930,7 +966,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingReadCloser() {
|
||||||
suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
|
suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
|
||||||
|
|
||||||
// now make sure the attachment is in the database
|
// now make sure the attachment is in the database
|
||||||
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
|
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(dbAttachment)
|
suite.NotNil(dbAttachment)
|
||||||
|
|
||||||
|
@ -959,7 +995,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingReadCloser() {
|
||||||
suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
|
suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ManagerTestSuite) TestPngNoAlphaChannelProcessBlocking() {
|
func (suite *ManagerTestSuite) TestPngNoAlphaChannelProcess() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
||||||
|
@ -974,18 +1010,22 @@ func (suite *ManagerTestSuite) TestPngNoAlphaChannelProcessBlocking() {
|
||||||
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
|
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
|
||||||
|
|
||||||
// process the media with no additional info provided
|
// process the media with no additional info provided
|
||||||
processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
|
processing, err := suite.manager.CreateMedia(ctx,
|
||||||
// fetch the attachment id from the processing media
|
accountID,
|
||||||
attachmentID := processingMedia.AttachmentID()
|
data,
|
||||||
|
media.AdditionalMediaInfo{},
|
||||||
|
)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(processing)
|
||||||
|
|
||||||
// do a blocking call to fetch the attachment
|
// do a blocking call to fetch the attachment
|
||||||
attachment, err := processingMedia.LoadAttachment(ctx)
|
attachment, err := processing.Load(ctx)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(attachment)
|
suite.NotNil(attachment)
|
||||||
|
|
||||||
// make sure it's got the stuff set on it that we expect
|
// make sure it's got the stuff set on it that we expect
|
||||||
// the attachment ID and accountID we expect
|
// the attachment ID and accountID we expect
|
||||||
suite.Equal(attachmentID, attachment.ID)
|
suite.Equal(processing.ID(), attachment.ID)
|
||||||
suite.Equal(accountID, attachment.AccountID)
|
suite.Equal(accountID, attachment.AccountID)
|
||||||
|
|
||||||
// file meta should be correctly derived from the image
|
// file meta should be correctly derived from the image
|
||||||
|
@ -1001,7 +1041,7 @@ func (suite *ManagerTestSuite) TestPngNoAlphaChannelProcessBlocking() {
|
||||||
suite.Equal("LFQT7e.A%O%4?co$M}M{_1W9~TxV", attachment.Blurhash)
|
suite.Equal("LFQT7e.A%O%4?co$M}M{_1W9~TxV", attachment.Blurhash)
|
||||||
|
|
||||||
// now make sure the attachment is in the database
|
// now make sure the attachment is in the database
|
||||||
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
|
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(dbAttachment)
|
suite.NotNil(dbAttachment)
|
||||||
|
|
||||||
|
@ -1030,7 +1070,7 @@ func (suite *ManagerTestSuite) TestPngNoAlphaChannelProcessBlocking() {
|
||||||
suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
|
suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ManagerTestSuite) TestPngAlphaChannelProcessBlocking() {
|
func (suite *ManagerTestSuite) TestPngAlphaChannelProcess() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
||||||
|
@ -1045,18 +1085,22 @@ func (suite *ManagerTestSuite) TestPngAlphaChannelProcessBlocking() {
|
||||||
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
|
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
|
||||||
|
|
||||||
// process the media with no additional info provided
|
// process the media with no additional info provided
|
||||||
processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
|
processing, err := suite.manager.CreateMedia(ctx,
|
||||||
// fetch the attachment id from the processing media
|
accountID,
|
||||||
attachmentID := processingMedia.AttachmentID()
|
data,
|
||||||
|
media.AdditionalMediaInfo{},
|
||||||
|
)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(processing)
|
||||||
|
|
||||||
// do a blocking call to fetch the attachment
|
// do a blocking call to fetch the attachment
|
||||||
attachment, err := processingMedia.LoadAttachment(ctx)
|
attachment, err := processing.Load(ctx)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(attachment)
|
suite.NotNil(attachment)
|
||||||
|
|
||||||
// make sure it's got the stuff set on it that we expect
|
// make sure it's got the stuff set on it that we expect
|
||||||
// the attachment ID and accountID we expect
|
// the attachment ID and accountID we expect
|
||||||
suite.Equal(attachmentID, attachment.ID)
|
suite.Equal(processing.ID(), attachment.ID)
|
||||||
suite.Equal(accountID, attachment.AccountID)
|
suite.Equal(accountID, attachment.AccountID)
|
||||||
|
|
||||||
// file meta should be correctly derived from the image
|
// file meta should be correctly derived from the image
|
||||||
|
@ -1072,7 +1116,7 @@ func (suite *ManagerTestSuite) TestPngAlphaChannelProcessBlocking() {
|
||||||
suite.Equal("LFQT7e.A%O%4?co$M}M{_1W9~TxV", attachment.Blurhash)
|
suite.Equal("LFQT7e.A%O%4?co$M}M{_1W9~TxV", attachment.Blurhash)
|
||||||
|
|
||||||
// now make sure the attachment is in the database
|
// now make sure the attachment is in the database
|
||||||
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
|
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(dbAttachment)
|
suite.NotNil(dbAttachment)
|
||||||
|
|
||||||
|
@ -1101,7 +1145,7 @@ func (suite *ManagerTestSuite) TestPngAlphaChannelProcessBlocking() {
|
||||||
suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
|
suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingWithCallback() {
|
func (suite *ManagerTestSuite) TestSimpleJpegProcessWithCallback() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
||||||
|
@ -1116,18 +1160,22 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingWithCallback() {
|
||||||
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
|
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
|
||||||
|
|
||||||
// process the media with no additional info provided
|
// process the media with no additional info provided
|
||||||
processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
|
processing, err := suite.manager.CreateMedia(ctx,
|
||||||
// fetch the attachment id from the processing media
|
accountID,
|
||||||
attachmentID := processingMedia.AttachmentID()
|
data,
|
||||||
|
media.AdditionalMediaInfo{},
|
||||||
|
)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(processing)
|
||||||
|
|
||||||
// do a blocking call to fetch the attachment
|
// do a blocking call to fetch the attachment
|
||||||
attachment, err := processingMedia.LoadAttachment(ctx)
|
attachment, err := processing.Load(ctx)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(attachment)
|
suite.NotNil(attachment)
|
||||||
|
|
||||||
// make sure it's got the stuff set on it that we expect
|
// make sure it's got the stuff set on it that we expect
|
||||||
// the attachment ID and accountID we expect
|
// the attachment ID and accountID we expect
|
||||||
suite.Equal(attachmentID, attachment.ID)
|
suite.Equal(processing.ID(), attachment.ID)
|
||||||
suite.Equal(accountID, attachment.AccountID)
|
suite.Equal(accountID, attachment.AccountID)
|
||||||
|
|
||||||
// file meta should be correctly derived from the image
|
// file meta should be correctly derived from the image
|
||||||
|
@ -1143,7 +1191,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingWithCallback() {
|
||||||
suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
|
suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
|
||||||
|
|
||||||
// now make sure the attachment is in the database
|
// now make sure the attachment is in the database
|
||||||
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
|
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(dbAttachment)
|
suite.NotNil(dbAttachment)
|
||||||
|
|
||||||
|
@ -1172,7 +1220,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingWithCallback() {
|
||||||
suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
|
suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingWithDiskStorage() {
|
func (suite *ManagerTestSuite) TestSimpleJpegProcessWithDiskStorage() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
||||||
|
@ -1209,18 +1257,22 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingWithDiskStorage() {
|
||||||
suite.manager = diskManager
|
suite.manager = diskManager
|
||||||
|
|
||||||
// process the media with no additional info provided
|
// process the media with no additional info provided
|
||||||
processingMedia := diskManager.PreProcessMedia(data, accountID, nil)
|
processing, err := suite.manager.CreateMedia(ctx,
|
||||||
// fetch the attachment id from the processing media
|
accountID,
|
||||||
attachmentID := processingMedia.AttachmentID()
|
data,
|
||||||
|
media.AdditionalMediaInfo{},
|
||||||
|
)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(processing)
|
||||||
|
|
||||||
// do a blocking call to fetch the attachment
|
// do a blocking call to fetch the attachment
|
||||||
attachment, err := processingMedia.LoadAttachment(ctx)
|
attachment, err := processing.Load(ctx)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(attachment)
|
suite.NotNil(attachment)
|
||||||
|
|
||||||
// make sure it's got the stuff set on it that we expect
|
// make sure it's got the stuff set on it that we expect
|
||||||
// the attachment ID and accountID we expect
|
// the attachment ID and accountID we expect
|
||||||
suite.Equal(attachmentID, attachment.ID)
|
suite.Equal(processing.ID(), attachment.ID)
|
||||||
suite.Equal(accountID, attachment.AccountID)
|
suite.Equal(accountID, attachment.AccountID)
|
||||||
|
|
||||||
// file meta should be correctly derived from the image
|
// file meta should be correctly derived from the image
|
||||||
|
@ -1236,7 +1288,7 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingWithDiskStorage() {
|
||||||
suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
|
suite.Equal("LiBzRk#6V[WF_NvzV@WY_3rqV@a$", attachment.Blurhash)
|
||||||
|
|
||||||
// now make sure the attachment is in the database
|
// now make sure the attachment is in the database
|
||||||
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
|
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(dbAttachment)
|
suite.NotNil(dbAttachment)
|
||||||
|
|
||||||
|
@ -1307,22 +1359,27 @@ func (suite *ManagerTestSuite) TestSmallSizedMediaTypeDetection_issue2263() {
|
||||||
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
|
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
|
||||||
|
|
||||||
// process the media with no additional info provided
|
// process the media with no additional info provided
|
||||||
processingMedia := suite.manager.PreProcessMedia(data, accountID, nil)
|
processing, err := suite.manager.CreateMedia(ctx,
|
||||||
if _, err := processingMedia.LoadAttachment(ctx); err != nil {
|
accountID,
|
||||||
suite.FailNow(err.Error())
|
data,
|
||||||
}
|
media.AdditionalMediaInfo{},
|
||||||
|
)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(processing)
|
||||||
|
|
||||||
attachmentID := processingMedia.AttachmentID()
|
// Load the attachment (but ignore return).
|
||||||
|
_, err = processing.Load(ctx)
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
// fetch the attachment id from the processing media
|
// fetch the attachment id from the processing media
|
||||||
attachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
|
attachment, err := suite.db.GetAttachmentByID(ctx, processing.ID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
suite.FailNow(err.Error())
|
suite.FailNow(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure it's got the stuff set on it that we expect
|
// make sure it's got the stuff set on it that we expect
|
||||||
// the attachment ID and accountID we expect
|
// the attachment ID and accountID we expect
|
||||||
suite.Equal(attachmentID, attachment.ID)
|
suite.Equal(processing.ID(), attachment.ID)
|
||||||
suite.Equal(accountID, attachment.AccountID)
|
suite.Equal(accountID, attachment.AccountID)
|
||||||
|
|
||||||
actual := attachment.File.ContentType
|
actual := attachment.File.ContentType
|
||||||
|
@ -1350,13 +1407,21 @@ func (suite *ManagerTestSuite) TestMisreportedSmallMedia() {
|
||||||
return io.NopCloser(bytes.NewBuffer(b)), int64(2 * actualSize), nil
|
return io.NopCloser(bytes.NewBuffer(b)), int64(2 * actualSize), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process the media with no additional info provided.
|
ctx := context.Background()
|
||||||
attachment, err := suite.manager.
|
|
||||||
PreProcessMedia(data, accountID, nil).
|
// process the media with no additional info provided
|
||||||
LoadAttachment(context.Background())
|
processing, err := suite.manager.CreateMedia(ctx,
|
||||||
if err != nil {
|
accountID,
|
||||||
suite.FailNow(err.Error())
|
data,
|
||||||
}
|
media.AdditionalMediaInfo{},
|
||||||
|
)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(processing)
|
||||||
|
|
||||||
|
// do a blocking call to fetch the attachment
|
||||||
|
attachment, err := processing.Load(ctx)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(attachment)
|
||||||
|
|
||||||
suite.Equal(actualSize, attachment.File.FileSize)
|
suite.Equal(actualSize, attachment.File.FileSize)
|
||||||
}
|
}
|
||||||
|
@ -1378,13 +1443,21 @@ func (suite *ManagerTestSuite) TestNoReportedSizeSmallMedia() {
|
||||||
return io.NopCloser(bytes.NewBuffer(b)), 0, nil
|
return io.NopCloser(bytes.NewBuffer(b)), 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process the media with no additional info provided.
|
ctx := context.Background()
|
||||||
attachment, err := suite.manager.
|
|
||||||
PreProcessMedia(data, accountID, nil).
|
// process the media with no additional info provided
|
||||||
LoadAttachment(context.Background())
|
processing, err := suite.manager.CreateMedia(ctx,
|
||||||
if err != nil {
|
accountID,
|
||||||
suite.FailNow(err.Error())
|
data,
|
||||||
}
|
media.AdditionalMediaInfo{},
|
||||||
|
)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(processing)
|
||||||
|
|
||||||
|
// do a blocking call to fetch the attachment
|
||||||
|
attachment, err := processing.Load(ctx)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(attachment)
|
||||||
|
|
||||||
suite.Equal(actualSize, attachment.File.FileSize)
|
suite.Equal(actualSize, attachment.File.FileSize)
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,14 +24,16 @@ import (
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"codeberg.org/gruf/go-bytesize"
|
"codeberg.org/gruf/go-bytesize"
|
||||||
"codeberg.org/gruf/go-errors/v2"
|
errorsv2 "codeberg.org/gruf/go-errors/v2"
|
||||||
"codeberg.org/gruf/go-runners"
|
"codeberg.org/gruf/go-runners"
|
||||||
"github.com/h2non/filetype"
|
"github.com/h2non/filetype"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/regexes"
|
"github.com/superseriousbusiness/gotosocial/internal/regexes"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/storage"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
@ -40,7 +42,6 @@ import (
|
||||||
// various functions for retrieving data from the process.
|
// various functions for retrieving data from the process.
|
||||||
type ProcessingEmoji struct {
|
type ProcessingEmoji struct {
|
||||||
emoji *gtsmodel.Emoji // processing emoji details
|
emoji *gtsmodel.Emoji // processing emoji details
|
||||||
existing bool // indicates whether this is an existing emoji ID being refreshed / recached
|
|
||||||
newPathID string // new emoji path ID to use when being refreshed
|
newPathID string // new emoji path ID to use when being refreshed
|
||||||
dataFn DataFunc // load-data function, returns media stream
|
dataFn DataFunc // load-data function, returns media stream
|
||||||
done bool // done is set when process finishes with non ctx canceled type error
|
done bool // done is set when process finishes with non ctx canceled type error
|
||||||
|
@ -49,61 +50,72 @@ type ProcessingEmoji struct {
|
||||||
mgr *Manager // mgr instance (access to db / storage)
|
mgr *Manager // mgr instance (access to db / storage)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EmojiID returns the ID of the underlying emoji without blocking processing.
|
// ID returns the ID of the underlying emoji.
|
||||||
func (p *ProcessingEmoji) EmojiID() string {
|
func (p *ProcessingEmoji) ID() string {
|
||||||
return p.emoji.ID // immutable, safe outside mutex.
|
return p.emoji.ID // immutable, safe outside mutex.
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadEmoji blocks until the static and fullsize image has been processed, and then returns the completed emoji.
|
// LoadEmoji blocks until the static and fullsize image has been processed, and then returns the completed emoji.
|
||||||
func (p *ProcessingEmoji) LoadEmoji(ctx context.Context) (*gtsmodel.Emoji, error) {
|
func (p *ProcessingEmoji) Load(ctx context.Context) (*gtsmodel.Emoji, error) {
|
||||||
// Attempt to load synchronously.
|
|
||||||
emoji, done, err := p.load(ctx)
|
emoji, done, err := p.load(ctx)
|
||||||
if err == nil {
|
|
||||||
// No issue, return media.
|
|
||||||
return emoji, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !done {
|
if !done {
|
||||||
// Provided context was cancelled, e.g. request cancelled
|
// On a context-canceled error (marked as !done), requeue for loading.
|
||||||
// early. Queue this item for asynchronous processing.
|
p.mgr.state.Workers.Dereference.Queue.Push(func(ctx context.Context) {
|
||||||
log.Warnf(ctx, "reprocessing emoji %s after canceled ctx", p.emoji.ID)
|
|
||||||
p.mgr.state.Workers.Media.Queue.Push(p.Process)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process allows the receiving object to fit the runners.WorkerFunc signature. It performs a (blocking) load and logs on error.
|
|
||||||
func (p *ProcessingEmoji) Process(ctx context.Context) {
|
|
||||||
if _, _, err := p.load(ctx); err != nil {
|
if _, _, err := p.load(ctx); err != nil {
|
||||||
log.Errorf(ctx, "error processing emoji: %v", err)
|
log.Errorf(ctx, "error loading emoji: %v", err)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return emoji, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// load performs a concurrency-safe load of ProcessingEmoji, only marking itself as complete when returned error is NOT a context cancel.
|
// load is the package private form of load() that is wrapped to catch context canceled.
|
||||||
func (p *ProcessingEmoji) load(ctx context.Context) (*gtsmodel.Emoji, bool, error) {
|
func (p *ProcessingEmoji) load(ctx context.Context) (
|
||||||
var (
|
emoji *gtsmodel.Emoji,
|
||||||
done bool
|
done bool,
|
||||||
err error
|
err error,
|
||||||
)
|
) {
|
||||||
|
|
||||||
err = p.proc.Process(func() error {
|
err = p.proc.Process(func() error {
|
||||||
if p.done {
|
if done = p.done; done {
|
||||||
// Already proc'd.
|
// Already proc'd.
|
||||||
return p.err
|
return p.err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
// This is only done when ctx NOT cancelled.
|
// This is only done when ctx NOT cancelled.
|
||||||
done = err == nil || !errors.IsV2(err,
|
done = (err == nil || !errorsv2.IsV2(err,
|
||||||
context.Canceled,
|
context.Canceled,
|
||||||
context.DeadlineExceeded,
|
context.DeadlineExceeded,
|
||||||
)
|
))
|
||||||
|
|
||||||
if !done {
|
if !done {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Anything from here, we
|
||||||
|
// need to ensure happens
|
||||||
|
// (i.e. no ctx canceled).
|
||||||
|
ctx = gtscontext.WithValues(
|
||||||
|
context.Background(),
|
||||||
|
ctx, // values
|
||||||
|
)
|
||||||
|
|
||||||
|
// On error, clean
|
||||||
|
// downloaded files.
|
||||||
|
if err != nil {
|
||||||
|
p.cleanup(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !done {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update with latest details, whatever happened.
|
||||||
|
e := p.mgr.state.DB.UpdateEmoji(ctx, p.emoji)
|
||||||
|
if e != nil {
|
||||||
|
log.Errorf(ctx, "error updating emoji in db: %v", e)
|
||||||
|
}
|
||||||
|
|
||||||
// Store final values.
|
// Store final values.
|
||||||
p.done = true
|
p.done = true
|
||||||
p.err = err
|
p.err = err
|
||||||
|
@ -111,39 +123,31 @@ func (p *ProcessingEmoji) load(ctx context.Context) (*gtsmodel.Emoji, bool, erro
|
||||||
|
|
||||||
// Attempt to store media and calculate
|
// Attempt to store media and calculate
|
||||||
// full-size media attachment details.
|
// full-size media attachment details.
|
||||||
|
//
|
||||||
|
// This will update p.emoji as it goes.
|
||||||
if err = p.store(ctx); err != nil {
|
if err = p.store(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finish processing by reloading media into
|
// Finish processing by reloading media into
|
||||||
// memory to get dimension and generate a thumb.
|
// memory to get dimension and generate a thumb.
|
||||||
|
//
|
||||||
|
// This will update p.emoji as it goes.
|
||||||
if err = p.finish(ctx); err != nil {
|
if err = p.finish(ctx); err != nil {
|
||||||
return err
|
return err //nolint:revive
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.existing {
|
return nil
|
||||||
// Existing emoji we're updating, so only update.
|
|
||||||
err = p.mgr.state.DB.UpdateEmoji(ctx, p.emoji)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// New emoji media, first time caching.
|
|
||||||
err = p.mgr.state.DB.PutEmoji(ctx, p.emoji)
|
|
||||||
return err
|
|
||||||
})
|
})
|
||||||
|
emoji = p.emoji
|
||||||
if err != nil {
|
return
|
||||||
return nil, done, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.emoji, done, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// store calls the data function attached to p if it hasn't been called yet,
|
// store calls the data function attached to p if it hasn't been called yet,
|
||||||
// and updates the underlying attachment fields as necessary. It will then stream
|
// and updates the underlying attachment fields as necessary. It will then stream
|
||||||
// bytes from p's reader directly into storage so that it can be retrieved later.
|
// bytes from p's reader directly into storage so that it can be retrieved later.
|
||||||
func (p *ProcessingEmoji) store(ctx context.Context) error {
|
func (p *ProcessingEmoji) store(ctx context.Context) error {
|
||||||
// Load media from provided data fn.
|
// Load media from provided data fun
|
||||||
rc, sz, err := p.dataFn(ctx)
|
rc, sz, err := p.dataFn(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gtserror.Newf("error executing data function: %w", err)
|
return gtserror.Newf("error executing data function: %w", err)
|
||||||
|
@ -168,8 +172,9 @@ func (p *ProcessingEmoji) store(ctx context.Context) error {
|
||||||
|
|
||||||
// Check that provided size isn't beyond max. We check beforehand
|
// Check that provided size isn't beyond max. We check beforehand
|
||||||
// so that we don't attempt to stream the emoji into storage if not needed.
|
// so that we don't attempt to stream the emoji into storage if not needed.
|
||||||
if size := bytesize.Size(sz); sz > 0 && size > maxSize {
|
if sz > 0 && sz > int64(maxSize) {
|
||||||
return gtserror.Newf("given emoji size %s greater than max allowed %s", size, maxSize)
|
sz := bytesize.Size(sz) // improves log readability
|
||||||
|
return gtserror.Newf("given emoji size %s greater than max allowed %s", sz, maxSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare to read bytes from
|
// Prepare to read bytes from
|
||||||
|
@ -196,14 +201,14 @@ func (p *ProcessingEmoji) store(ctx context.Context) error {
|
||||||
|
|
||||||
// Initial file size was misreported, so we didn't read
|
// Initial file size was misreported, so we didn't read
|
||||||
// fully into hdrBuf. Reslice it to the size we did read.
|
// fully into hdrBuf. Reslice it to the size we did read.
|
||||||
log.Warnf(ctx,
|
|
||||||
"recovered from misreported file size; reported %d; read %d",
|
|
||||||
fileSize, n,
|
|
||||||
)
|
|
||||||
hdrBuf = hdrBuf[:n]
|
hdrBuf = hdrBuf[:n]
|
||||||
|
fileSize = n
|
||||||
|
p.emoji.ImageFileSize = fileSize
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse file type info from header buffer.
|
// Parse file type info from header buffer.
|
||||||
|
// This should only ever error if the buffer
|
||||||
|
// is empty (ie., the attachment is 0 bytes).
|
||||||
info, err := filetype.Match(hdrBuf)
|
info, err := filetype.Match(hdrBuf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gtserror.Newf("error parsing file type: %w", err)
|
return gtserror.Newf("error parsing file type: %w", err)
|
||||||
|
@ -227,10 +232,13 @@ func (p *ProcessingEmoji) store(ctx context.Context) error {
|
||||||
pathID = p.emoji.ID
|
pathID = p.emoji.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine instance account ID from already generated image static path.
|
// Determine instance account ID from generated image static path.
|
||||||
instanceAccID := regexes.FilePath.FindStringSubmatch(p.emoji.ImageStaticPath)[1]
|
instanceAccID, ok := getInstanceAccountID(p.emoji.ImageStaticPath)
|
||||||
|
if !ok {
|
||||||
|
return gtserror.Newf("invalid emoji static path; no instance account id: %s", p.emoji.ImageStaticPath)
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate emoji file path.
|
// Calculate final media attachment file path.
|
||||||
p.emoji.ImagePath = uris.StoragePathForAttachment(
|
p.emoji.ImagePath = uris.StoragePathForAttachment(
|
||||||
instanceAccID,
|
instanceAccID,
|
||||||
string(TypeEmoji),
|
string(TypeEmoji),
|
||||||
|
@ -239,32 +247,32 @@ func (p *ProcessingEmoji) store(ctx context.Context) error {
|
||||||
info.Extension,
|
info.Extension,
|
||||||
)
|
)
|
||||||
|
|
||||||
// This shouldn't already exist, but we do a check as it's worth logging.
|
// File shouldn't already exist in storage at this point,
|
||||||
|
// but we do a check as it's worth logging / cleaning up.
|
||||||
if have, _ := p.mgr.state.Storage.Has(ctx, p.emoji.ImagePath); have {
|
if have, _ := p.mgr.state.Storage.Has(ctx, p.emoji.ImagePath); have {
|
||||||
log.Warnf(ctx, "emoji already exists at storage path: %s", p.emoji.ImagePath)
|
log.Warnf(ctx, "emoji already exists at: %s", p.emoji.ImagePath)
|
||||||
|
|
||||||
// Attempt to remove existing emoji at storage path (might be broken / out-of-date)
|
// Attempt to remove existing emoji at storage path (might be broken / out-of-date)
|
||||||
if err := p.mgr.state.Storage.Delete(ctx, p.emoji.ImagePath); err != nil {
|
if err := p.mgr.state.Storage.Delete(ctx, p.emoji.ImagePath); err != nil {
|
||||||
return gtserror.Newf("error removing emoji from storage: %v", err)
|
return gtserror.Newf("error removing emoji %s from storage: %v", p.emoji.ImagePath, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the final image reader stream to our storage.
|
// Write the final image reader stream to our storage.
|
||||||
wroteSize, err := p.mgr.state.Storage.PutStream(ctx, p.emoji.ImagePath, r)
|
sz, err = p.mgr.state.Storage.PutStream(ctx, p.emoji.ImagePath, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gtserror.Newf("error writing emoji to storage: %w", err)
|
return gtserror.Newf("error writing emoji to storage: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Once again check size in case none was provided previously.
|
// Perform final size check in case none was
|
||||||
if size := bytesize.Size(wroteSize); size > maxSize {
|
// given previously, or size was mis-reported.
|
||||||
if err := p.mgr.state.Storage.Delete(ctx, p.emoji.ImagePath); err != nil {
|
// (error here will later perform p.cleanup()).
|
||||||
log.Errorf(ctx, "error removing too-large-emoji from storage: %v", err)
|
if sz > int64(maxSize) {
|
||||||
|
sz := bytesize.Size(sz) // improves log readability
|
||||||
|
return gtserror.Newf("written emoji size %s greater than max allowed %s", sz, maxSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
return gtserror.Newf("calculated emoji size %s greater than max allowed %s", size, maxSize)
|
// Fill in remaining emoji data now it's stored.
|
||||||
}
|
|
||||||
|
|
||||||
// Fill in remaining attachment data now it's stored.
|
|
||||||
p.emoji.ImageURL = uris.URIForAttachment(
|
p.emoji.ImageURL = uris.URIForAttachment(
|
||||||
instanceAccID,
|
instanceAccID,
|
||||||
string(TypeEmoji),
|
string(TypeEmoji),
|
||||||
|
@ -273,14 +281,14 @@ func (p *ProcessingEmoji) store(ctx context.Context) error {
|
||||||
info.Extension,
|
info.Extension,
|
||||||
)
|
)
|
||||||
p.emoji.ImageContentType = info.MIME.Value
|
p.emoji.ImageContentType = info.MIME.Value
|
||||||
p.emoji.ImageFileSize = int(wroteSize)
|
p.emoji.ImageFileSize = int(sz)
|
||||||
p.emoji.Cached = util.Ptr(true)
|
p.emoji.Cached = util.Ptr(true)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ProcessingEmoji) finish(ctx context.Context) error {
|
func (p *ProcessingEmoji) finish(ctx context.Context) error {
|
||||||
// Fetch a stream to the original file in storage.
|
// Get a stream to the original file for further processing.
|
||||||
rc, err := p.mgr.state.Storage.GetStream(ctx, p.emoji.ImagePath)
|
rc, err := p.mgr.state.Storage.GetStream(ctx, p.emoji.ImagePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gtserror.Newf("error loading file from storage: %w", err)
|
return gtserror.Newf("error loading file from storage: %w", err)
|
||||||
|
@ -293,32 +301,69 @@ func (p *ProcessingEmoji) finish(ctx context.Context) error {
|
||||||
return gtserror.Newf("error decoding image: %w", err)
|
return gtserror.Newf("error decoding image: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The image should be in-memory by now.
|
// staticImg should be in-memory by
|
||||||
|
// now so we're done with storage.
|
||||||
if err := rc.Close(); err != nil {
|
if err := rc.Close(); err != nil {
|
||||||
return gtserror.Newf("error closing file: %w", err)
|
return gtserror.Newf("error closing file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// This shouldn't already exist, but we do a check as it's worth logging.
|
// Static img shouldn't exist in storage at this point,
|
||||||
|
// but we do a check as it's worth logging / cleaning up.
|
||||||
if have, _ := p.mgr.state.Storage.Has(ctx, p.emoji.ImageStaticPath); have {
|
if have, _ := p.mgr.state.Storage.Has(ctx, p.emoji.ImageStaticPath); have {
|
||||||
log.Warnf(ctx, "static emoji already exists at storage path: %s", p.emoji.ImagePath)
|
log.Warnf(ctx, "static emoji already exists at: %s", p.emoji.ImageStaticPath)
|
||||||
|
|
||||||
// Attempt to remove static existing emoji at storage path (might be broken / out-of-date)
|
// Attempt to remove existing thumbnail (might be broken / out-of-date).
|
||||||
if err := p.mgr.state.Storage.Delete(ctx, p.emoji.ImageStaticPath); err != nil {
|
if err := p.mgr.state.Storage.Delete(ctx, p.emoji.ImageStaticPath); err != nil {
|
||||||
return gtserror.Newf("error removing static emoji from storage: %v", err)
|
return gtserror.Newf("error removing static emoji %s from storage: %v", p.emoji.ImageStaticPath, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create an emoji PNG encoder stream.
|
// Create emoji PNG encoder stream.
|
||||||
enc := staticImg.ToPNG()
|
enc := staticImg.ToPNG()
|
||||||
|
|
||||||
// Stream-encode the PNG static image into storage.
|
// Stream-encode the PNG static emoji image into our storage driver.
|
||||||
sz, err := p.mgr.state.Storage.PutStream(ctx, p.emoji.ImageStaticPath, enc)
|
sz, err := p.mgr.state.Storage.PutStream(ctx, p.emoji.ImageStaticPath, enc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gtserror.Newf("error stream-encoding static emoji to storage: %w", err)
|
return gtserror.Newf("error stream-encoding static emoji to storage: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set written image size.
|
// Set final written thumb size.
|
||||||
p.emoji.ImageStaticFileSize = int(sz)
|
p.emoji.ImageStaticFileSize = int(sz)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cleanup will remove any traces of processing emoji from storage,
|
||||||
|
// and perform any other necessary cleanup steps after failure.
|
||||||
|
func (p *ProcessingEmoji) cleanup(ctx context.Context) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if p.emoji.ImagePath != "" {
|
||||||
|
// Ensure emoji file at path is deleted from storage.
|
||||||
|
err = p.mgr.state.Storage.Delete(ctx, p.emoji.ImagePath)
|
||||||
|
if err != nil && !storage.IsNotFound(err) {
|
||||||
|
log.Errorf(ctx, "error deleting %s: %v", p.emoji.ImagePath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.emoji.ImageStaticPath != "" {
|
||||||
|
// Ensure emoji static file at path is deleted from storage.
|
||||||
|
err = p.mgr.state.Storage.Delete(ctx, p.emoji.ImageStaticPath)
|
||||||
|
if err != nil && !storage.IsNotFound(err) {
|
||||||
|
log.Errorf(ctx, "error deleting %s: %v", p.emoji.ImageStaticPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure marked as not cached.
|
||||||
|
p.emoji.Cached = util.Ptr(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getInstanceAccountID determines the instance account ID from
|
||||||
|
// emoji static image storage path. returns false on failure.
|
||||||
|
func getInstanceAccountID(staticPath string) (string, bool) {
|
||||||
|
matches := regexes.FilePath.FindStringSubmatch(staticPath)
|
||||||
|
if len(matches) < 2 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return matches[1], true
|
||||||
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ package media
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
"io"
|
"io"
|
||||||
|
@ -29,6 +30,7 @@ import (
|
||||||
terminator "codeberg.org/superseriousbusiness/exif-terminator"
|
terminator "codeberg.org/superseriousbusiness/exif-terminator"
|
||||||
"github.com/disintegration/imaging"
|
"github.com/disintegration/imaging"
|
||||||
"github.com/h2non/filetype"
|
"github.com/h2non/filetype"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
|
@ -43,16 +45,14 @@ import (
|
||||||
type ProcessingMedia struct {
|
type ProcessingMedia struct {
|
||||||
media *gtsmodel.MediaAttachment // processing media attachment details
|
media *gtsmodel.MediaAttachment // processing media attachment details
|
||||||
dataFn DataFunc // load-data function, returns media stream
|
dataFn DataFunc // load-data function, returns media stream
|
||||||
recache bool // recaching existing (uncached) media
|
|
||||||
done bool // done is set when process finishes with non ctx canceled type error
|
done bool // done is set when process finishes with non ctx canceled type error
|
||||||
proc runners.Processor // proc helps synchronize only a singular running processing instance
|
proc runners.Processor // proc helps synchronize only a singular running processing instance
|
||||||
err error // error stores permanent error value when done
|
err error // error stores permanent error value when done
|
||||||
mgr *Manager // mgr instance (access to db / storage)
|
mgr *Manager // mgr instance (access to db / storage)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AttachmentID returns the ID of the underlying
|
// ID returns the ID of the underlying media.
|
||||||
// media attachment without blocking processing.
|
func (p *ProcessingMedia) ID() string {
|
||||||
func (p *ProcessingMedia) AttachmentID() string {
|
|
||||||
return p.media.ID // immutable, safe outside mutex.
|
return p.media.ID // immutable, safe outside mutex.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,124 +65,102 @@ func (p *ProcessingMedia) AttachmentID() string {
|
||||||
// will still be returned in that case, but it will
|
// will still be returned in that case, but it will
|
||||||
// only be partially complete and should be treated
|
// only be partially complete and should be treated
|
||||||
// as a placeholder.
|
// as a placeholder.
|
||||||
func (p *ProcessingMedia) LoadAttachment(ctx context.Context) (*gtsmodel.MediaAttachment, error) {
|
func (p *ProcessingMedia) Load(ctx context.Context) (*gtsmodel.MediaAttachment, error) {
|
||||||
// Attempt to load synchronously.
|
|
||||||
media, done, err := p.load(ctx)
|
media, done, err := p.load(ctx)
|
||||||
if err == nil {
|
|
||||||
// No issue, return media.
|
|
||||||
return media, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !done {
|
if !done {
|
||||||
// Provided context was cancelled,
|
// On a context-canceled error (marked as !done), requeue for loading.
|
||||||
// e.g. request aborted early before
|
p.mgr.state.Workers.Dereference.Queue.Push(func(ctx context.Context) {
|
||||||
// its context could be used to finish
|
if _, _, err := p.load(ctx); err != nil {
|
||||||
// loading the attachment. Enqueue for
|
log.Errorf(ctx, "error loading media: %v", err)
|
||||||
// asynchronous processing, which will
|
}
|
||||||
// use a background context.
|
})
|
||||||
log.Warnf(ctx, "reprocessing media %s after canceled ctx", p.media.ID)
|
|
||||||
p.mgr.state.Workers.Media.Queue.Push(p.Process)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Media could not be retrieved FULLY,
|
|
||||||
// but partial attachment should be present.
|
|
||||||
return media, err
|
return media, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process allows the receiving object to fit the
|
// load is the package private form of load() that is wrapped to catch context canceled.
|
||||||
// runners.WorkerFunc signature. It performs a
|
func (p *ProcessingMedia) load(ctx context.Context) (
|
||||||
// (blocking) load and logs on error.
|
media *gtsmodel.MediaAttachment,
|
||||||
func (p *ProcessingMedia) Process(ctx context.Context) {
|
done bool,
|
||||||
if _, _, err := p.load(ctx); err != nil {
|
err error,
|
||||||
log.Errorf(ctx, "error(s) processing media: %v", err)
|
) {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// load performs a concurrency-safe load of ProcessingMedia, only
|
|
||||||
// marking itself as complete when returned error is NOT a context cancel.
|
|
||||||
func (p *ProcessingMedia) load(ctx context.Context) (*gtsmodel.MediaAttachment, bool, error) {
|
|
||||||
var (
|
|
||||||
done bool
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
err = p.proc.Process(func() error {
|
err = p.proc.Process(func() error {
|
||||||
if p.done {
|
if done = p.done; done {
|
||||||
// Already proc'd.
|
// Already proc'd.
|
||||||
return p.err
|
return p.err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
// This is only done when ctx NOT cancelled.
|
// This is only done when ctx NOT cancelled.
|
||||||
done = err == nil || !errorsv2.IsV2(err,
|
done = (err == nil || !errorsv2.IsV2(err,
|
||||||
context.Canceled,
|
context.Canceled,
|
||||||
context.DeadlineExceeded,
|
context.DeadlineExceeded,
|
||||||
)
|
))
|
||||||
|
|
||||||
if !done {
|
if !done {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Anything from here, we
|
||||||
|
// need to ensure happens
|
||||||
|
// (i.e. no ctx canceled).
|
||||||
|
ctx = gtscontext.WithValues(
|
||||||
|
context.Background(),
|
||||||
|
ctx, // values
|
||||||
|
)
|
||||||
|
|
||||||
|
// On error or unknown media types, perform error cleanup.
|
||||||
|
if err != nil || p.media.Type == gtsmodel.FileTypeUnknown {
|
||||||
|
p.cleanup(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update with latest details, whatever happened.
|
||||||
|
e := p.mgr.state.DB.UpdateAttachment(ctx, p.media)
|
||||||
|
if e != nil {
|
||||||
|
log.Errorf(ctx, "error updating media in db: %v", e)
|
||||||
|
}
|
||||||
|
|
||||||
// Store final values.
|
// Store final values.
|
||||||
p.done = true
|
p.done = true
|
||||||
p.err = err
|
p.err = err
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Gather errors as we proceed.
|
// TODO: in time update this
|
||||||
var errs = gtserror.NewMultiError(4)
|
// to perhaps follow a similar
|
||||||
|
// freshness window to statuses
|
||||||
|
// / accounts? But that's a big
|
||||||
|
// maybe, media don't change in
|
||||||
|
// the same way so this is largely
|
||||||
|
// just to slow down fail retries.
|
||||||
|
const maxfreq = 6 * time.Hour
|
||||||
|
|
||||||
|
// Check whether media is uncached but repeatedly failing,
|
||||||
|
// specifically limit the frequency at which we allow this.
|
||||||
|
if !p.media.UpdatedAt.Equal(p.media.CreatedAt) && // i.e. not new
|
||||||
|
p.media.UpdatedAt.Add(maxfreq).Before(time.Now()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Attempt to store media and calculate
|
// Attempt to store media and calculate
|
||||||
// full-size media attachment details.
|
// full-size media attachment details.
|
||||||
//
|
//
|
||||||
// This will update p.media as it goes.
|
// This will update p.media as it goes.
|
||||||
storeErr := p.store(ctx)
|
if err = p.store(ctx); err != nil {
|
||||||
if storeErr != nil {
|
return err
|
||||||
errs.Append(storeErr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finish processing by reloading media into
|
// Finish processing by reloading media into
|
||||||
// memory to get dimension and generate a thumb.
|
// memory to get dimension and generate a thumb.
|
||||||
//
|
//
|
||||||
// This will update p.media as it goes.
|
// This will update p.media as it goes.
|
||||||
if finishErr := p.finish(ctx); finishErr != nil {
|
if err = p.finish(ctx); err != nil {
|
||||||
errs.Append(finishErr)
|
return err //nolint:revive
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this isn't a file we were able to process,
|
return nil
|
||||||
// we may have partially stored it (eg., it's a
|
|
||||||
// jpeg, which is fine, but streaming it to storage
|
|
||||||
// was interrupted halfway through and so it was
|
|
||||||
// never decoded). Try to clean up in this case.
|
|
||||||
if p.media.Type == gtsmodel.FileTypeUnknown {
|
|
||||||
deleteErr := p.mgr.state.Storage.Delete(ctx, p.media.File.Path)
|
|
||||||
if deleteErr != nil && !storage.IsNotFound(deleteErr) {
|
|
||||||
errs.Append(deleteErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var dbErr error
|
|
||||||
switch {
|
|
||||||
case !p.recache:
|
|
||||||
// First time caching this attachment, insert it.
|
|
||||||
dbErr = p.mgr.state.DB.PutAttachment(ctx, p.media)
|
|
||||||
|
|
||||||
case p.recache && len(errs) == 0:
|
|
||||||
// Existing attachment we're recaching, update it.
|
|
||||||
//
|
|
||||||
// (We only want to update if everything went OK so far,
|
|
||||||
// otherwise we'd better leave previous version alone.)
|
|
||||||
dbErr = p.mgr.state.DB.UpdateAttachment(ctx, p.media)
|
|
||||||
}
|
|
||||||
|
|
||||||
if dbErr != nil {
|
|
||||||
errs.Append(dbErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = errs.Combine()
|
|
||||||
return err
|
|
||||||
})
|
})
|
||||||
|
media = p.media
|
||||||
return p.media, done, err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// store calls the data function attached to p if it hasn't been called yet,
|
// store calls the data function attached to p if it hasn't been called yet,
|
||||||
|
@ -231,10 +209,6 @@ func (p *ProcessingMedia) store(ctx context.Context) error {
|
||||||
|
|
||||||
// Initial file size was misreported, so we didn't read
|
// Initial file size was misreported, so we didn't read
|
||||||
// fully into hdrBuf. Reslice it to the size we did read.
|
// fully into hdrBuf. Reslice it to the size we did read.
|
||||||
log.Warnf(ctx,
|
|
||||||
"recovered from misreported file size; reported %d; read %d",
|
|
||||||
fileSize, n,
|
|
||||||
)
|
|
||||||
hdrBuf = hdrBuf[:n]
|
hdrBuf = hdrBuf[:n]
|
||||||
fileSize = n
|
fileSize = n
|
||||||
p.media.File.FileSize = fileSize
|
p.media.File.FileSize = fileSize
|
||||||
|
@ -273,20 +247,13 @@ func (p *ProcessingMedia) store(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// The file is not a supported format that
|
// The file is not a supported format that we can process, so we can't do much with it.
|
||||||
// we can process, so we can't do much with it.
|
log.Warnf(ctx, "unsupported media extension '%s'; not caching locally", info.Extension)
|
||||||
log.Warnf(ctx,
|
|
||||||
"media extension '%s' not officially supported, will be processed as "+
|
|
||||||
"type '%s' with minimal metadata, and will not be cached locally",
|
|
||||||
info.Extension, gtsmodel.FileTypeUnknown,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Don't bother storing this.
|
|
||||||
store = false
|
store = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill in correct attachment
|
// Fill in correct attachment
|
||||||
// data now we're parsed it.
|
// data now we've parsed it.
|
||||||
p.media.URL = uris.URIForAttachment(
|
p.media.URL = uris.URIForAttachment(
|
||||||
p.media.AccountID,
|
p.media.AccountID,
|
||||||
string(TypeAttachment),
|
string(TypeAttachment),
|
||||||
|
@ -295,15 +262,11 @@ func (p *ProcessingMedia) store(ctx context.Context) error {
|
||||||
info.Extension,
|
info.Extension,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Prefer discovered mime type, fall back to
|
// Prefer discovered MIME, fallback to generic data stream.
|
||||||
// generic "this contains some bytes" type.
|
mime := cmp.Or(info.MIME.Value, "application/octet-stream")
|
||||||
mime := info.MIME.Value
|
|
||||||
if mime == "" {
|
|
||||||
mime = "application/octet-stream"
|
|
||||||
}
|
|
||||||
p.media.File.ContentType = mime
|
p.media.File.ContentType = mime
|
||||||
|
|
||||||
// Calculate attachment file path.
|
// Calculate final media attachment file path.
|
||||||
p.media.File.Path = uris.StoragePathForAttachment(
|
p.media.File.Path = uris.StoragePathForAttachment(
|
||||||
p.media.AccountID,
|
p.media.AccountID,
|
||||||
string(TypeAttachment),
|
string(TypeAttachment),
|
||||||
|
@ -323,23 +286,23 @@ func (p *ProcessingMedia) store(ctx context.Context) error {
|
||||||
// File shouldn't already exist in storage at this point,
|
// File shouldn't already exist in storage at this point,
|
||||||
// but we do a check as it's worth logging / cleaning up.
|
// but we do a check as it's worth logging / cleaning up.
|
||||||
if have, _ := p.mgr.state.Storage.Has(ctx, p.media.File.Path); have {
|
if have, _ := p.mgr.state.Storage.Has(ctx, p.media.File.Path); have {
|
||||||
log.Warnf(ctx, "media already exists at storage path: %s", p.media.File.Path)
|
log.Warnf(ctx, "media already exists at: %s", p.media.File.Path)
|
||||||
|
|
||||||
// Attempt to remove existing media at storage path (might be broken / out-of-date)
|
// Attempt to remove existing media at storage path (might be broken / out-of-date)
|
||||||
if err := p.mgr.state.Storage.Delete(ctx, p.media.File.Path); err != nil {
|
if err := p.mgr.state.Storage.Delete(ctx, p.media.File.Path); err != nil {
|
||||||
return gtserror.Newf("error removing media from storage: %v", err)
|
return gtserror.Newf("error removing media %s from storage: %v", p.media.File.Path, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the final reader stream to our storage.
|
// Write the final reader stream to our storage driver.
|
||||||
wroteSize, err := p.mgr.state.Storage.PutStream(ctx, p.media.File.Path, r)
|
sz, err = p.mgr.state.Storage.PutStream(ctx, p.media.File.Path, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gtserror.Newf("error writing media to storage: %w", err)
|
return gtserror.Newf("error writing media to storage: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set actual written size
|
// Set actual written size
|
||||||
// as authoritative file size.
|
// as authoritative file size.
|
||||||
p.media.File.FileSize = int(wroteSize)
|
p.media.File.FileSize = int(sz)
|
||||||
|
|
||||||
// We can now consider this cached.
|
// We can now consider this cached.
|
||||||
p.media.Cached = util.Ptr(true)
|
p.media.Cached = util.Ptr(true)
|
||||||
|
@ -348,36 +311,9 @@ func (p *ProcessingMedia) store(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ProcessingMedia) finish(ctx context.Context) error {
|
func (p *ProcessingMedia) finish(ctx context.Context) error {
|
||||||
// Make a jolly assumption about thumbnail type.
|
// Nothing else to do if
|
||||||
p.media.Thumbnail.ContentType = mimeImageJpeg
|
// media was not cached.
|
||||||
|
|
||||||
// Calculate attachment thumbnail file path
|
|
||||||
p.media.Thumbnail.Path = uris.StoragePathForAttachment(
|
|
||||||
p.media.AccountID,
|
|
||||||
string(TypeAttachment),
|
|
||||||
string(SizeSmall),
|
|
||||||
p.media.ID,
|
|
||||||
// Always encode attachment
|
|
||||||
// thumbnails as jpg.
|
|
||||||
"jpg",
|
|
||||||
)
|
|
||||||
|
|
||||||
// Calculate attachment thumbnail serve path.
|
|
||||||
p.media.Thumbnail.URL = uris.URIForAttachment(
|
|
||||||
p.media.AccountID,
|
|
||||||
string(TypeAttachment),
|
|
||||||
string(SizeSmall),
|
|
||||||
p.media.ID,
|
|
||||||
// Always encode attachment
|
|
||||||
// thumbnails as jpg.
|
|
||||||
"jpg",
|
|
||||||
)
|
|
||||||
|
|
||||||
// If original file hasn't been stored, there's
|
|
||||||
// likely something wrong with the data, or we
|
|
||||||
// don't want to store it. Skip everything else.
|
|
||||||
if !*p.media.Cached {
|
if !*p.media.Cached {
|
||||||
p.media.Processing = gtsmodel.ProcessingStatusProcessed
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -398,8 +334,7 @@ func (p *ProcessingMedia) finish(ctx context.Context) error {
|
||||||
|
|
||||||
// .jpeg, .gif, .webp image type
|
// .jpeg, .gif, .webp image type
|
||||||
case mimeImageJpeg, mimeImageGif, mimeImageWebp:
|
case mimeImageJpeg, mimeImageGif, mimeImageWebp:
|
||||||
fullImg, err = decodeImage(
|
fullImg, err = decodeImage(rc,
|
||||||
rc,
|
|
||||||
imaging.AutoOrientation(true),
|
imaging.AutoOrientation(true),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -451,9 +386,9 @@ func (p *ProcessingMedia) finish(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set full-size dimensions in attachment info.
|
// Set full-size dimensions in attachment info.
|
||||||
p.media.FileMeta.Original.Width = int(fullImg.Width())
|
p.media.FileMeta.Original.Width = fullImg.Width()
|
||||||
p.media.FileMeta.Original.Height = int(fullImg.Height())
|
p.media.FileMeta.Original.Height = fullImg.Height()
|
||||||
p.media.FileMeta.Original.Size = int(fullImg.Size())
|
p.media.FileMeta.Original.Size = fullImg.Size()
|
||||||
p.media.FileMeta.Original.Aspect = fullImg.AspectRatio()
|
p.media.FileMeta.Original.Aspect = fullImg.AspectRatio()
|
||||||
|
|
||||||
// Get smaller thumbnail image
|
// Get smaller thumbnail image
|
||||||
|
@ -475,44 +410,72 @@ func (p *ProcessingMedia) finish(ctx context.Context) error {
|
||||||
p.media.Blurhash = hash
|
p.media.Blurhash = hash
|
||||||
}
|
}
|
||||||
|
|
||||||
// Thumbnail shouldn't already exist in storage at this point,
|
// Thumbnail shouldn't exist in storage at this point,
|
||||||
// but we do a check as it's worth logging / cleaning up.
|
// but we do a check as it's worth logging / cleaning up.
|
||||||
if have, _ := p.mgr.state.Storage.Has(ctx, p.media.Thumbnail.Path); have {
|
if have, _ := p.mgr.state.Storage.Has(ctx, p.media.Thumbnail.Path); have {
|
||||||
log.Warnf(ctx, "thumbnail already exists at storage path: %s", p.media.Thumbnail.Path)
|
log.Warnf(ctx, "thumbnail already exists at: %s", p.media.Thumbnail.Path)
|
||||||
|
|
||||||
// Attempt to remove existing thumbnail at storage path (might be broken / out-of-date)
|
// Attempt to remove existing thumbnail (might be broken / out-of-date).
|
||||||
if err := p.mgr.state.Storage.Delete(ctx, p.media.Thumbnail.Path); err != nil {
|
if err := p.mgr.state.Storage.Delete(ctx, p.media.Thumbnail.Path); err != nil {
|
||||||
return gtserror.Newf("error removing thumbnail from storage: %v", err)
|
return gtserror.Newf("error removing thumbnail %s from storage: %v", p.media.Thumbnail.Path, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a thumbnail JPEG encoder stream.
|
// Create a thumbnail JPEG encoder stream.
|
||||||
enc := thumbImg.ToJPEG(&jpeg.Options{
|
enc := thumbImg.ToJPEG(&jpeg.Options{
|
||||||
|
|
||||||
// Good enough for
|
// Good enough for
|
||||||
// a thumbnail.
|
// a thumbnail.
|
||||||
Quality: 70,
|
Quality: 70,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Stream-encode the JPEG thumbnail image into storage.
|
// Stream-encode the JPEG thumbnail image into our storage driver.
|
||||||
sz, err := p.mgr.state.Storage.PutStream(ctx, p.media.Thumbnail.Path, enc)
|
sz, err := p.mgr.state.Storage.PutStream(ctx, p.media.Thumbnail.Path, enc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gtserror.Newf("error stream-encoding thumbnail to storage: %w", err)
|
return gtserror.Newf("error stream-encoding thumbnail to storage: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set final written thumb size.
|
||||||
|
p.media.Thumbnail.FileSize = int(sz)
|
||||||
|
|
||||||
// Set thumbnail dimensions in attachment info.
|
// Set thumbnail dimensions in attachment info.
|
||||||
p.media.FileMeta.Small = gtsmodel.Small{
|
p.media.FileMeta.Small = gtsmodel.Small{
|
||||||
Width: int(thumbImg.Width()),
|
Width: thumbImg.Width(),
|
||||||
Height: int(thumbImg.Height()),
|
Height: thumbImg.Height(),
|
||||||
Size: int(thumbImg.Size()),
|
Size: thumbImg.Size(),
|
||||||
Aspect: thumbImg.AspectRatio(),
|
Aspect: thumbImg.AspectRatio(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set written image size.
|
// Finally set the attachment as processed.
|
||||||
p.media.Thumbnail.FileSize = int(sz)
|
|
||||||
|
|
||||||
// Finally set the attachment as processed and update time.
|
|
||||||
p.media.Processing = gtsmodel.ProcessingStatusProcessed
|
p.media.Processing = gtsmodel.ProcessingStatusProcessed
|
||||||
p.media.File.UpdatedAt = time.Now()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cleanup will remove any traces of processing media from storage.
|
||||||
|
// and perform any other necessary cleanup steps after failure.
|
||||||
|
func (p *ProcessingMedia) cleanup(ctx context.Context) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if p.media.File.Path != "" {
|
||||||
|
// Ensure media file at path is deleted from storage.
|
||||||
|
err = p.mgr.state.Storage.Delete(ctx, p.media.File.Path)
|
||||||
|
if err != nil && !storage.IsNotFound(err) {
|
||||||
|
log.Errorf(ctx, "error deleting %s: %v", p.media.File.Path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.media.Thumbnail.Path != "" {
|
||||||
|
// Ensure media thumbnail at path is deleted from storage.
|
||||||
|
err = p.mgr.state.Storage.Delete(ctx, p.media.Thumbnail.Path)
|
||||||
|
if err != nil && !storage.IsNotFound(err) {
|
||||||
|
log.Errorf(ctx, "error deleting %s: %v", p.media.Thumbnail.Path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also ensure marked as unknown and finished
|
||||||
|
// processing so gets inserted as placeholder URL.
|
||||||
|
p.media.Processing = gtsmodel.ProcessingStatusProcessed
|
||||||
|
p.media.Type = gtsmodel.FileTypeUnknown
|
||||||
|
p.media.Cached = util.Ptr(false)
|
||||||
|
}
|
||||||
|
|
|
@ -112,19 +112,19 @@ func (m *Manager) RefetchEmojis(ctx context.Context, domain string, dereferenceM
|
||||||
return dereferenceMedia(ctx, emojiImageIRI)
|
return dereferenceMedia(ctx, emojiImageIRI)
|
||||||
}
|
}
|
||||||
|
|
||||||
processingEmoji, err := m.PreProcessEmoji(ctx, dataFunc, emoji.Shortcode, emoji.ID, emoji.URI, &AdditionalEmojiInfo{
|
processingEmoji, err := m.RefreshEmoji(ctx, emoji, dataFunc, AdditionalEmojiInfo{
|
||||||
Domain: &emoji.Domain,
|
Domain: &emoji.Domain,
|
||||||
ImageRemoteURL: &emoji.ImageRemoteURL,
|
ImageRemoteURL: &emoji.ImageRemoteURL,
|
||||||
ImageStaticRemoteURL: &emoji.ImageStaticRemoteURL,
|
ImageStaticRemoteURL: &emoji.ImageStaticRemoteURL,
|
||||||
Disabled: emoji.Disabled,
|
Disabled: emoji.Disabled,
|
||||||
VisibleInPicker: emoji.VisibleInPicker,
|
VisibleInPicker: emoji.VisibleInPicker,
|
||||||
}, true)
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(ctx, "emoji %s could not be refreshed because of an error during processing: %s", shortcodeDomain, err)
|
log.Errorf(ctx, "emoji %s could not be refreshed because of an error during processing: %s", shortcodeDomain, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := processingEmoji.LoadEmoji(ctx); err != nil {
|
if _, err := processingEmoji.Load(ctx); err != nil {
|
||||||
log.Errorf(ctx, "emoji %s could not be refreshed because of an error during loading: %s", shortcodeDomain, err)
|
log.Errorf(ctx, "emoji %s could not be refreshed because of an error during loading: %s", shortcodeDomain, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,47 +61,85 @@ const (
|
||||||
TypeEmoji Type = "emoji" // TypeEmoji is the key for emoji type requests
|
TypeEmoji Type = "emoji" // TypeEmoji is the key for emoji type requests
|
||||||
)
|
)
|
||||||
|
|
||||||
// AdditionalMediaInfo represents additional information that should be added to an attachment
|
// AdditionalMediaInfo represents additional information that
|
||||||
// when processing a piece of media.
|
// should be added to attachment when processing a piece of media.
|
||||||
type AdditionalMediaInfo struct {
|
type AdditionalMediaInfo struct {
|
||||||
// Time that this media was created; defaults to time.Now().
|
|
||||||
|
// Time that this media was
|
||||||
|
// created; defaults to time.Now().
|
||||||
CreatedAt *time.Time
|
CreatedAt *time.Time
|
||||||
// ID of the status to which this media is attached; defaults to "".
|
|
||||||
|
// ID of the status to which this
|
||||||
|
// media is attached; defaults to "".
|
||||||
StatusID *string
|
StatusID *string
|
||||||
// URL of the media on a remote instance; defaults to "".
|
|
||||||
|
// URL of the media on a
|
||||||
|
// remote instance; defaults to "".
|
||||||
RemoteURL *string
|
RemoteURL *string
|
||||||
// Image description of this media; defaults to "".
|
|
||||||
|
// Image description of
|
||||||
|
// this media; defaults to "".
|
||||||
Description *string
|
Description *string
|
||||||
// Blurhash of this media; defaults to "".
|
|
||||||
|
// Blurhash of this
|
||||||
|
// media; defaults to "".
|
||||||
Blurhash *string
|
Blurhash *string
|
||||||
// ID of the scheduled status to which this media is attached; defaults to "".
|
|
||||||
|
// ID of the scheduled status to which
|
||||||
|
// this media is attached; defaults to "".
|
||||||
ScheduledStatusID *string
|
ScheduledStatusID *string
|
||||||
// Mark this media as in-use as an avatar; defaults to false.
|
|
||||||
|
// Mark this media as in-use
|
||||||
|
// as an avatar; defaults to false.
|
||||||
Avatar *bool
|
Avatar *bool
|
||||||
// Mark this media as in-use as a header; defaults to false.
|
|
||||||
|
// Mark this media as in-use
|
||||||
|
// as a header; defaults to false.
|
||||||
Header *bool
|
Header *bool
|
||||||
// X focus coordinate for this media; defaults to 0.
|
|
||||||
|
// X focus coordinate for
|
||||||
|
// this media; defaults to 0.
|
||||||
FocusX *float32
|
FocusX *float32
|
||||||
// Y focus coordinate for this media; defaults to 0.
|
|
||||||
|
// Y focus coordinate for
|
||||||
|
// this media; defaults to 0.
|
||||||
FocusY *float32
|
FocusY *float32
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdditionalEmojiInfo represents additional information
|
// AdditionalEmojiInfo represents additional information
|
||||||
// that should be taken into account when processing an emoji.
|
// that should be taken into account when processing an emoji.
|
||||||
type AdditionalEmojiInfo struct {
|
type AdditionalEmojiInfo struct {
|
||||||
// Time that this emoji was created; defaults to time.Now().
|
|
||||||
|
// ActivityPub URI of
|
||||||
|
// this remote emoji.
|
||||||
|
URI *string
|
||||||
|
|
||||||
|
// Time that this emoji was
|
||||||
|
// created; defaults to time.Now().
|
||||||
CreatedAt *time.Time
|
CreatedAt *time.Time
|
||||||
// Domain the emoji originated from. Blank for this instance's domain. Defaults to "".
|
|
||||||
|
// Domain the emoji originated from. Blank
|
||||||
|
// for this instance's domain. Defaults to "".
|
||||||
Domain *string
|
Domain *string
|
||||||
// URL of this emoji on a remote instance; defaults to "".
|
|
||||||
|
// URL of this emoji on a
|
||||||
|
// remote instance; defaults to "".
|
||||||
ImageRemoteURL *string
|
ImageRemoteURL *string
|
||||||
// URL of the static version of this emoji on a remote instance; defaults to "".
|
|
||||||
|
// URL of the static version of this emoji
|
||||||
|
// on a remote instance; defaults to "".
|
||||||
ImageStaticRemoteURL *string
|
ImageStaticRemoteURL *string
|
||||||
// Whether this emoji should be disabled (not shown) on this instance; defaults to false.
|
|
||||||
|
// Whether this emoji should be disabled (not
|
||||||
|
// shown) on this instance; defaults to false.
|
||||||
Disabled *bool
|
Disabled *bool
|
||||||
// Whether this emoji should be visible in the instance's emoji picker; defaults to true.
|
|
||||||
|
// Whether this emoji should be visible in
|
||||||
|
// the instance's emoji picker; defaults to true.
|
||||||
VisibleInPicker *bool
|
VisibleInPicker *bool
|
||||||
// ID of the category this emoji should be placed in; defaults to "".
|
|
||||||
|
// ID of the category this emoji
|
||||||
|
// should be placed in; defaults to "".
|
||||||
CategoryID *string
|
CategoryID *string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,5 @@ func newHdrBuf(fileSize int) []byte {
|
||||||
if fileSize > 0 && fileSize < bufSize {
|
if fileSize > 0 && fileSize < bufSize {
|
||||||
bufSize = fileSize
|
bufSize = fileSize
|
||||||
}
|
}
|
||||||
|
|
||||||
return make([]byte, bufSize)
|
return make([]byte, bufSize)
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,7 +111,7 @@ func (suite *AccountStandardTestSuite) SetupTest() {
|
||||||
suite.emailSender = testrig.NewEmailSender("../../../web/template/", suite.sentEmails)
|
suite.emailSender = testrig.NewEmailSender("../../../web/template/", suite.sentEmails)
|
||||||
|
|
||||||
filter := visibility.NewFilter(&suite.state)
|
filter := visibility.NewFilter(&suite.state)
|
||||||
common := common.New(&suite.state, suite.tc, suite.federator, filter)
|
common := common.New(&suite.state, suite.mediaManager, suite.tc, suite.federator, filter)
|
||||||
suite.accountProcessor = account.New(&common, &suite.state, suite.tc, suite.mediaManager, suite.federator, filter, processing.GetParseMentionFunc(&suite.state, suite.federator))
|
suite.accountProcessor = account.New(&common, &suite.state, suite.tc, suite.mediaManager, suite.federator, filter, processing.GetParseMentionFunc(&suite.state, suite.federator))
|
||||||
testrig.StandardDBSetup(suite.db, nil)
|
testrig.StandardDBSetup(suite.db, nil)
|
||||||
testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
|
testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
|
||||||
|
|
|
@ -19,10 +19,12 @@ package account
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
|
|
||||||
|
"codeberg.org/gruf/go-bytesize"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
@ -203,9 +205,13 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.Avatar != nil && form.Avatar.Size != 0 {
|
if form.Avatar != nil && form.Avatar.Size != 0 {
|
||||||
avatarInfo, err := p.UpdateAvatar(ctx, form.Avatar, nil, account.ID)
|
avatarInfo, errWithCode := p.UpdateAvatar(ctx,
|
||||||
if err != nil {
|
account,
|
||||||
return nil, gtserror.NewErrorBadRequest(err)
|
form.Avatar,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if errWithCode != nil {
|
||||||
|
return nil, errWithCode
|
||||||
}
|
}
|
||||||
account.AvatarMediaAttachmentID = avatarInfo.ID
|
account.AvatarMediaAttachmentID = avatarInfo.ID
|
||||||
account.AvatarMediaAttachment = avatarInfo
|
account.AvatarMediaAttachment = avatarInfo
|
||||||
|
@ -213,9 +219,13 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.Header != nil && form.Header.Size != 0 {
|
if form.Header != nil && form.Header.Size != 0 {
|
||||||
headerInfo, err := p.UpdateHeader(ctx, form.Header, nil, account.ID)
|
headerInfo, errWithCode := p.UpdateHeader(ctx,
|
||||||
if err != nil {
|
account,
|
||||||
return nil, gtserror.NewErrorBadRequest(err)
|
form.Header,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if errWithCode != nil {
|
||||||
|
return nil, errWithCode
|
||||||
}
|
}
|
||||||
account.HeaderMediaAttachmentID = headerInfo.ID
|
account.HeaderMediaAttachmentID = headerInfo.ID
|
||||||
account.HeaderMediaAttachment = headerInfo
|
account.HeaderMediaAttachment = headerInfo
|
||||||
|
@ -316,35 +326,33 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
|
||||||
// for this to become the account's new avatar.
|
// for this to become the account's new avatar.
|
||||||
func (p *Processor) UpdateAvatar(
|
func (p *Processor) UpdateAvatar(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
account *gtsmodel.Account,
|
||||||
avatar *multipart.FileHeader,
|
avatar *multipart.FileHeader,
|
||||||
description *string,
|
description *string,
|
||||||
accountID string,
|
) (
|
||||||
) (*gtsmodel.MediaAttachment, error) {
|
*gtsmodel.MediaAttachment,
|
||||||
maxImageSize := config.GetMediaImageMaxSize()
|
gtserror.WithCode,
|
||||||
if avatar.Size > int64(maxImageSize) {
|
) {
|
||||||
return nil, gtserror.Newf("size %d exceeded max media size of %d bytes", avatar.Size, maxImageSize)
|
max := config.GetMediaImageMaxSize()
|
||||||
|
if sz := bytesize.Size(avatar.Size); sz > max {
|
||||||
|
text := fmt.Sprintf("size %s exceeds max media size %s", sz, max)
|
||||||
|
return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||||
}
|
}
|
||||||
|
|
||||||
data := func(innerCtx context.Context) (io.ReadCloser, int64, error) {
|
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
||||||
f, err := avatar.Open()
|
f, err := avatar.Open()
|
||||||
return f, avatar.Size, err
|
return f, avatar.Size, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process the media attachment and load it immediately.
|
// Write to instance storage.
|
||||||
media := p.mediaManager.PreProcessMedia(data, accountID, &media.AdditionalMediaInfo{
|
return p.c.StoreLocalMedia(ctx,
|
||||||
|
account.ID,
|
||||||
|
data,
|
||||||
|
media.AdditionalMediaInfo{
|
||||||
Avatar: util.Ptr(true),
|
Avatar: util.Ptr(true),
|
||||||
Description: description,
|
Description: description,
|
||||||
})
|
},
|
||||||
|
)
|
||||||
attachment, err := media.LoadAttachment(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
|
||||||
} else if attachment.Type == gtsmodel.FileTypeUnknown {
|
|
||||||
err = gtserror.Newf("could not process uploaded file with extension %s", attachment.File.ContentType)
|
|
||||||
return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return attachment, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateHeader does the dirty work of checking the header
|
// UpdateHeader does the dirty work of checking the header
|
||||||
|
@ -353,33 +361,31 @@ func (p *Processor) UpdateAvatar(
|
||||||
// for this to become the account's new header.
|
// for this to become the account's new header.
|
||||||
func (p *Processor) UpdateHeader(
|
func (p *Processor) UpdateHeader(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
account *gtsmodel.Account,
|
||||||
header *multipart.FileHeader,
|
header *multipart.FileHeader,
|
||||||
description *string,
|
description *string,
|
||||||
accountID string,
|
) (
|
||||||
) (*gtsmodel.MediaAttachment, error) {
|
*gtsmodel.MediaAttachment,
|
||||||
maxImageSize := config.GetMediaImageMaxSize()
|
gtserror.WithCode,
|
||||||
if header.Size > int64(maxImageSize) {
|
) {
|
||||||
return nil, gtserror.Newf("size %d exceeded max media size of %d bytes", header.Size, maxImageSize)
|
max := config.GetMediaImageMaxSize()
|
||||||
|
if sz := bytesize.Size(header.Size); sz > max {
|
||||||
|
text := fmt.Sprintf("size %s exceeds max media size %s", sz, max)
|
||||||
|
return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||||
}
|
}
|
||||||
|
|
||||||
data := func(innerCtx context.Context) (io.ReadCloser, int64, error) {
|
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
||||||
f, err := header.Open()
|
f, err := header.Open()
|
||||||
return f, header.Size, err
|
return f, header.Size, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process the media attachment and load it immediately.
|
// Write to instance storage.
|
||||||
media := p.mediaManager.PreProcessMedia(data, accountID, &media.AdditionalMediaInfo{
|
return p.c.StoreLocalMedia(ctx,
|
||||||
|
account.ID,
|
||||||
|
data,
|
||||||
|
media.AdditionalMediaInfo{
|
||||||
Header: util.Ptr(true),
|
Header: util.Ptr(true),
|
||||||
Description: description,
|
Description: description,
|
||||||
})
|
},
|
||||||
|
)
|
||||||
attachment, err := media.LoadAttachment(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
|
||||||
} else if attachment.Type == gtsmodel.FileTypeUnknown {
|
|
||||||
err = gtserror.Newf("could not process uploaded file with extension %s", attachment.File.ContentType)
|
|
||||||
return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return attachment, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,20 +20,26 @@ package admin
|
||||||
import (
|
import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/cleaner"
|
"github.com/superseriousbusiness/gotosocial/internal/cleaner"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Processor struct {
|
type Processor struct {
|
||||||
|
// common processor logic
|
||||||
|
c *common.Processor
|
||||||
|
|
||||||
state *state.State
|
state *state.State
|
||||||
cleaner *cleaner.Cleaner
|
cleaner *cleaner.Cleaner
|
||||||
converter *typeutils.Converter
|
converter *typeutils.Converter
|
||||||
mediaManager *media.Manager
|
federator *federation.Federator
|
||||||
transportController transport.Controller
|
media *media.Manager
|
||||||
emailSender email.Sender
|
transport transport.Controller
|
||||||
|
email email.Sender
|
||||||
|
|
||||||
// admin Actions currently
|
// admin Actions currently
|
||||||
// undergoing processing
|
// undergoing processing
|
||||||
|
@ -46,21 +52,24 @@ func (p *Processor) Actions() *Actions {
|
||||||
|
|
||||||
// New returns a new admin processor.
|
// New returns a new admin processor.
|
||||||
func New(
|
func New(
|
||||||
|
common *common.Processor,
|
||||||
state *state.State,
|
state *state.State,
|
||||||
cleaner *cleaner.Cleaner,
|
cleaner *cleaner.Cleaner,
|
||||||
|
federator *federation.Federator,
|
||||||
converter *typeutils.Converter,
|
converter *typeutils.Converter,
|
||||||
mediaManager *media.Manager,
|
mediaManager *media.Manager,
|
||||||
transportController transport.Controller,
|
transportController transport.Controller,
|
||||||
emailSender email.Sender,
|
emailSender email.Sender,
|
||||||
) Processor {
|
) Processor {
|
||||||
return Processor{
|
return Processor{
|
||||||
|
c: common,
|
||||||
state: state,
|
state: state,
|
||||||
cleaner: cleaner,
|
cleaner: cleaner,
|
||||||
converter: converter,
|
converter: converter,
|
||||||
mediaManager: mediaManager,
|
federator: federator,
|
||||||
transportController: transportController,
|
media: mediaManager,
|
||||||
emailSender: emailSender,
|
transport: transportController,
|
||||||
|
email: emailSender,
|
||||||
actions: &Actions{
|
actions: &Actions{
|
||||||
r: make(map[string]*gtsmodel.AdminAction),
|
r: make(map[string]*gtsmodel.AdminAction),
|
||||||
state: state,
|
state: state,
|
||||||
|
|
|
@ -78,7 +78,7 @@ func (p *Processor) DebugAPUrl(
|
||||||
}
|
}
|
||||||
|
|
||||||
// All looks fine. Prepare the transport and (signed) GET request.
|
// All looks fine. Prepare the transport and (signed) GET request.
|
||||||
tsport, err := p.transportController.NewTransportForUsername(ctx, adminAcct.Username)
|
tsport, err := p.transport.NewTransportForUsername(ctx, adminAcct.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = gtserror.Newf("error creating transport: %w", err)
|
err = gtserror.Newf("error creating transport: %w", err)
|
||||||
return nil, gtserror.NewErrorInternalError(err, err.Error())
|
return nil, gtserror.NewErrorInternalError(err, err.Error())
|
||||||
|
|
|
@ -55,7 +55,7 @@ func (p *Processor) EmailTest(
|
||||||
InstanceName: instance.Title,
|
InstanceName: instance.Title,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.emailSender.SendTestEmail(toAddress, testData); err != nil {
|
if err := p.email.SendTestEmail(toAddress, testData); err != nil {
|
||||||
if gtserror.IsSMTP(err) {
|
if gtserror.IsSMTP(err) {
|
||||||
// An error occurred during the SMTP part.
|
// An error occurred during the SMTP part.
|
||||||
// We should indicate this to the caller, as
|
// We should indicate this to the caller, as
|
||||||
|
|
|
@ -31,7 +31,6 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -41,64 +40,21 @@ func (p *Processor) EmojiCreate(
|
||||||
account *gtsmodel.Account,
|
account *gtsmodel.Account,
|
||||||
form *apimodel.EmojiCreateRequest,
|
form *apimodel.EmojiCreateRequest,
|
||||||
) (*apimodel.Emoji, gtserror.WithCode) {
|
) (*apimodel.Emoji, gtserror.WithCode) {
|
||||||
// Ensure emoji with this shortcode
|
|
||||||
// doesn't already exist on the instance.
|
|
||||||
maybeExisting, err := p.state.DB.GetEmojiByShortcodeDomain(ctx, form.Shortcode, "")
|
|
||||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
|
||||||
err := gtserror.Newf("error checking existence of emoji with shortcode %s: %w", form.Shortcode, err)
|
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if maybeExisting != nil {
|
// Simply read provided form data for emoji data source.
|
||||||
err := fmt.Errorf("emoji with shortcode %s already exists", form.Shortcode)
|
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
||||||
return nil, gtserror.NewErrorConflict(err, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare data function for emoji processing
|
|
||||||
// (just read data from the submitted form).
|
|
||||||
data := func(innerCtx context.Context) (io.ReadCloser, int64, error) {
|
|
||||||
f, err := form.Image.Open()
|
f, err := form.Image.Open()
|
||||||
return f, form.Image.Size, err
|
return f, form.Image.Size, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If category was supplied on the form,
|
// Attempt to create the new local emoji.
|
||||||
// ensure the category exists and provide
|
emoji, errWithCode := p.createEmoji(ctx,
|
||||||
// it as additional info to emoji processing.
|
form.Shortcode,
|
||||||
var ai *media.AdditionalEmojiInfo
|
form.CategoryName,
|
||||||
if form.CategoryName != "" {
|
data,
|
||||||
category, err := p.getOrCreateEmojiCategory(ctx, form.CategoryName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ai = &media.AdditionalEmojiInfo{
|
|
||||||
CategoryID: &category.ID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate new emoji ID and URI.
|
|
||||||
emojiID, err := id.NewRandomULID()
|
|
||||||
if err != nil {
|
|
||||||
err := gtserror.Newf("error creating id for new emoji: %w", err)
|
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
emojiURI := uris.URIForEmoji(emojiID)
|
|
||||||
|
|
||||||
// Begin media processing.
|
|
||||||
processingEmoji, err := p.mediaManager.PreProcessEmoji(ctx,
|
|
||||||
data, form.Shortcode, emojiID, emojiURI, ai, false,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
err := gtserror.Newf("error processing emoji: %w", err)
|
return nil, errWithCode
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Complete processing immediately.
|
|
||||||
emoji, err := processingEmoji.LoadEmoji(ctx)
|
|
||||||
if err != nil {
|
|
||||||
err := gtserror.Newf("error loading emoji: %w", err)
|
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apiEmoji, err := p.converter.EmojiToAPIEmoji(ctx, emoji)
|
apiEmoji, err := p.converter.EmojiToAPIEmoji(ctx, emoji)
|
||||||
|
@ -110,53 +66,6 @@ func (p *Processor) EmojiCreate(
|
||||||
return &apiEmoji, nil
|
return &apiEmoji, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// emojisGetFilterParams builds extra
|
|
||||||
// query parameters to return as part
|
|
||||||
// of an Emojis pageable response.
|
|
||||||
//
|
|
||||||
// The returned string will look like:
|
|
||||||
//
|
|
||||||
// "filter=domain:all,enabled,shortcode:example"
|
|
||||||
func emojisGetFilterParams(
|
|
||||||
shortcode string,
|
|
||||||
domain string,
|
|
||||||
includeDisabled bool,
|
|
||||||
includeEnabled bool,
|
|
||||||
) string {
|
|
||||||
var filterBuilder strings.Builder
|
|
||||||
filterBuilder.WriteString("filter=")
|
|
||||||
|
|
||||||
switch domain {
|
|
||||||
case "", "local":
|
|
||||||
// Local emojis only.
|
|
||||||
filterBuilder.WriteString("domain:local")
|
|
||||||
|
|
||||||
case db.EmojiAllDomains:
|
|
||||||
// Local or remote.
|
|
||||||
filterBuilder.WriteString("domain:all")
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Specific domain only.
|
|
||||||
filterBuilder.WriteString("domain:" + domain)
|
|
||||||
}
|
|
||||||
|
|
||||||
if includeDisabled != includeEnabled {
|
|
||||||
if includeDisabled {
|
|
||||||
filterBuilder.WriteString(",disabled")
|
|
||||||
}
|
|
||||||
if includeEnabled {
|
|
||||||
filterBuilder.WriteString(",enabled")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if shortcode != "" {
|
|
||||||
// Specific shortcode only.
|
|
||||||
filterBuilder.WriteString(",shortcode:" + shortcode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return filterBuilder.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// EmojisGet returns an admin view of custom
|
// EmojisGet returns an admin view of custom
|
||||||
// emojis, filtered with the given parameters.
|
// emojis, filtered with the given parameters.
|
||||||
func (p *Processor) EmojisGet(
|
func (p *Processor) EmojisGet(
|
||||||
|
@ -287,21 +196,24 @@ func (p *Processor) EmojiDelete(
|
||||||
// given id, using the provided form parameters.
|
// given id, using the provided form parameters.
|
||||||
func (p *Processor) EmojiUpdate(
|
func (p *Processor) EmojiUpdate(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id string,
|
emojiID string,
|
||||||
form *apimodel.EmojiUpdateRequest,
|
form *apimodel.EmojiUpdateRequest,
|
||||||
) (*apimodel.AdminEmoji, gtserror.WithCode) {
|
) (*apimodel.AdminEmoji, gtserror.WithCode) {
|
||||||
emoji, err := p.state.DB.GetEmojiByID(ctx, id)
|
|
||||||
|
// Get the emoji with given ID from the database.
|
||||||
|
emoji, err := p.state.DB.GetEmojiByID(ctx, emojiID)
|
||||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
err := gtserror.Newf("db error: %w", err)
|
err := gtserror.Newf("error fetching emoji from db: %w", err)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check found.
|
||||||
if emoji == nil {
|
if emoji == nil {
|
||||||
err := gtserror.Newf("no emoji with id %s found in the db", id)
|
const text = "emoji not found"
|
||||||
return nil, gtserror.NewErrorNotFound(err)
|
return nil, gtserror.NewErrorNotFound(errors.New(text), text)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch t := form.Type; t {
|
switch form.Type {
|
||||||
|
|
||||||
case apimodel.EmojiUpdateCopy:
|
case apimodel.EmojiUpdateCopy:
|
||||||
return p.emojiUpdateCopy(ctx, emoji, form.Shortcode, form.CategoryName)
|
return p.emojiUpdateCopy(ctx, emoji, form.Shortcode, form.CategoryName)
|
||||||
|
@ -313,8 +225,8 @@ func (p *Processor) EmojiUpdate(
|
||||||
return p.emojiUpdateModify(ctx, emoji, form.Image, form.CategoryName)
|
return p.emojiUpdateModify(ctx, emoji, form.Image, form.CategoryName)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
err := fmt.Errorf("unrecognized emoji action type %s", t)
|
const text = "unrecognized emoji update action type"
|
||||||
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -342,56 +254,6 @@ func (p *Processor) EmojiCategoriesGet(
|
||||||
return apiCategories, nil
|
return apiCategories, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
UTIL FUNCTIONS
|
|
||||||
*/
|
|
||||||
|
|
||||||
// getOrCreateEmojiCategory either gets an existing
|
|
||||||
// category with the given name from the database,
|
|
||||||
// or, if the category doesn't yet exist, it creates
|
|
||||||
// the category and then returns it.
|
|
||||||
func (p *Processor) getOrCreateEmojiCategory(
|
|
||||||
ctx context.Context,
|
|
||||||
name string,
|
|
||||||
) (*gtsmodel.EmojiCategory, error) {
|
|
||||||
category, err := p.state.DB.GetEmojiCategoryByName(ctx, name)
|
|
||||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
|
||||||
return nil, gtserror.Newf(
|
|
||||||
"database error trying get emoji category %s: %w",
|
|
||||||
name, err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if category != nil {
|
|
||||||
// We had it already.
|
|
||||||
return category, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// We don't have the category yet,
|
|
||||||
// create it with the given name.
|
|
||||||
categoryID, err := id.NewRandomULID()
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.Newf(
|
|
||||||
"error generating id for new emoji category %s: %w",
|
|
||||||
name, err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
category = >smodel.EmojiCategory{
|
|
||||||
ID: categoryID,
|
|
||||||
Name: name,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.state.DB.PutEmojiCategory(ctx, category); err != nil {
|
|
||||||
return nil, gtserror.Newf(
|
|
||||||
"db error putting new emoji category %s: %w",
|
|
||||||
name, err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return category, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// emojiUpdateCopy copies and stores the given
|
// emojiUpdateCopy copies and stores the given
|
||||||
// *remote* emoji as a *local* emoji, preserving
|
// *remote* emoji as a *local* emoji, preserving
|
||||||
// the same image, and using the provided shortcode.
|
// the same image, and using the provided shortcode.
|
||||||
|
@ -400,99 +262,56 @@ func (p *Processor) getOrCreateEmojiCategory(
|
||||||
// emoji already stored in the database + storage.
|
// emoji already stored in the database + storage.
|
||||||
func (p *Processor) emojiUpdateCopy(
|
func (p *Processor) emojiUpdateCopy(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
targetEmoji *gtsmodel.Emoji,
|
target *gtsmodel.Emoji,
|
||||||
shortcode *string,
|
shortcode *string,
|
||||||
category *string,
|
categoryName *string,
|
||||||
) (*apimodel.AdminEmoji, gtserror.WithCode) {
|
) (*apimodel.AdminEmoji, gtserror.WithCode) {
|
||||||
if targetEmoji.IsLocal() {
|
if target.IsLocal() {
|
||||||
err := fmt.Errorf("emoji %s is not a remote emoji, cannot copy it to local", targetEmoji.ID)
|
const text = "target emoji is not remote; cannot copy to local"
|
||||||
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||||
}
|
}
|
||||||
|
|
||||||
if shortcode == nil {
|
// Ensure target emoji is locally cached.
|
||||||
err := errors.New("no shortcode provided")
|
target, err := p.federator.RefreshEmoji(
|
||||||
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
ctx,
|
||||||
}
|
target,
|
||||||
|
|
||||||
sc := *shortcode
|
// no changes we want to make.
|
||||||
if sc == "" {
|
media.AdditionalEmojiInfo{},
|
||||||
err := errors.New("empty shortcode provided")
|
false,
|
||||||
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
)
|
||||||
|
if err != nil {
|
||||||
|
err := gtserror.Newf("error recaching emoji %s: %w", target.ImageRemoteURL, err)
|
||||||
|
return nil, gtserror.NewErrorNotFound(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we don't already have an emoji
|
|
||||||
// stored locally with this shortcode.
|
|
||||||
maybeExisting, err := p.state.DB.GetEmojiByShortcodeDomain(ctx, sc, "")
|
|
||||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
|
||||||
err := gtserror.Newf("db error checking for emoji with shortcode %s: %w", sc, err)
|
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if maybeExisting != nil {
|
|
||||||
err := fmt.Errorf("emoji with shortcode %s already exists on this instance", sc)
|
|
||||||
return nil, gtserror.NewErrorConflict(err, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// We don't have an emoji with this
|
|
||||||
// shortcode yet! Prepare to create it.
|
|
||||||
|
|
||||||
// Data function for copying just streams media
|
// Data function for copying just streams media
|
||||||
// out of storage into an additional location.
|
// out of storage into an additional location.
|
||||||
//
|
//
|
||||||
// This means that data for the copy persists even
|
// This means that data for the copy persists even
|
||||||
// if the remote copied emoji gets deleted at some point.
|
// if the remote copied emoji gets deleted at some point.
|
||||||
data := func(ctx context.Context) (io.ReadCloser, int64, error) {
|
data := func(ctx context.Context) (io.ReadCloser, int64, error) {
|
||||||
rc, err := p.state.Storage.GetStream(ctx, targetEmoji.ImagePath)
|
rc, err := p.state.Storage.GetStream(ctx, target.ImagePath)
|
||||||
return rc, int64(targetEmoji.ImageFileSize), err
|
return rc, int64(target.ImageFileSize), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate new emoji ID and URI.
|
// Attempt to create the new local emoji.
|
||||||
emojiID, err := id.NewRandomULID()
|
emoji, errWithCode := p.createEmoji(ctx,
|
||||||
if err != nil {
|
util.PtrValueOr(shortcode, ""),
|
||||||
err := gtserror.Newf("error creating id for new emoji: %w", err)
|
util.PtrValueOr(categoryName, ""),
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
data,
|
||||||
}
|
|
||||||
|
|
||||||
emojiURI := uris.URIForEmoji(emojiID)
|
|
||||||
|
|
||||||
// If category was supplied, ensure the
|
|
||||||
// category exists and provide it as
|
|
||||||
// additional info to emoji processing.
|
|
||||||
var ai *media.AdditionalEmojiInfo
|
|
||||||
if category != nil && *category != "" {
|
|
||||||
category, err := p.getOrCreateEmojiCategory(ctx, *category)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ai = &media.AdditionalEmojiInfo{
|
|
||||||
CategoryID: &category.ID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Begin media processing.
|
|
||||||
processingEmoji, err := p.mediaManager.PreProcessEmoji(ctx,
|
|
||||||
data, sc, emojiID, emojiURI, ai, false,
|
|
||||||
)
|
)
|
||||||
|
if errWithCode != nil {
|
||||||
|
return nil, errWithCode
|
||||||
|
}
|
||||||
|
|
||||||
|
apiEmoji, err := p.converter.EmojiToAdminAPIEmoji(ctx, emoji)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := gtserror.Newf("error processing emoji: %w", err)
|
err := gtserror.Newf("error converting emoji: %w", err)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Complete processing immediately.
|
return apiEmoji, nil
|
||||||
newEmoji, err := processingEmoji.LoadEmoji(ctx)
|
|
||||||
if err != nil {
|
|
||||||
err := gtserror.Newf("error loading emoji: %w", err)
|
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
adminEmoji, err := p.converter.EmojiToAdminAPIEmoji(ctx, newEmoji)
|
|
||||||
if err != nil {
|
|
||||||
err := gtserror.Newf("error converting emoji %s to admin emoji: %w", newEmoji.ID, err)
|
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return adminEmoji, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// emojiUpdateDisable marks the given *remote*
|
// emojiUpdateDisable marks the given *remote*
|
||||||
|
@ -521,7 +340,7 @@ func (p *Processor) emojiUpdateDisable(
|
||||||
|
|
||||||
adminEmoji, err := p.converter.EmojiToAdminAPIEmoji(ctx, emoji)
|
adminEmoji, err := p.converter.EmojiToAdminAPIEmoji(ctx, emoji)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := gtserror.Newf("error converting emoji %s to admin emoji: %w", emoji.ID, err)
|
err := gtserror.Newf("error converting emoji: %w", err)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -541,104 +360,222 @@ func (p *Processor) emojiUpdateModify(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
emoji *gtsmodel.Emoji,
|
emoji *gtsmodel.Emoji,
|
||||||
image *multipart.FileHeader,
|
image *multipart.FileHeader,
|
||||||
category *string,
|
categoryName *string,
|
||||||
) (*apimodel.AdminEmoji, gtserror.WithCode) {
|
) (*apimodel.AdminEmoji, gtserror.WithCode) {
|
||||||
if !emoji.IsLocal() {
|
if !emoji.IsLocal() {
|
||||||
err := fmt.Errorf("emoji %s is not a local emoji, cannot update it via this endpoint", emoji.ID)
|
const text = "cannot modify remote emoji"
|
||||||
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure there's actually something to update.
|
// Ensure there's actually something to update.
|
||||||
if image == nil && category == nil {
|
if image == nil && categoryName == nil {
|
||||||
err := errors.New("neither new category nor new image set, cannot update")
|
const text = "no changes were provided"
|
||||||
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only update category
|
if categoryName != nil {
|
||||||
// if it's changed.
|
if *categoryName != "" {
|
||||||
var (
|
// A category was provided, get / create relevant emoji category.
|
||||||
newCategory *gtsmodel.EmojiCategory
|
category, errWithCode := p.mustGetEmojiCategory(ctx, *categoryName)
|
||||||
newCategoryID string
|
if errWithCode != nil {
|
||||||
updateCategoryID bool
|
return nil, errWithCode
|
||||||
)
|
|
||||||
|
|
||||||
if category != nil {
|
|
||||||
catName := *category
|
|
||||||
if catName != "" {
|
|
||||||
// Set new category.
|
|
||||||
var err error
|
|
||||||
newCategory, err = p.getOrCreateEmojiCategory(ctx, catName)
|
|
||||||
if err != nil {
|
|
||||||
err := gtserror.Newf("error getting or creating category: %w", err)
|
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
newCategoryID = newCategory.ID
|
if category.ID == emoji.CategoryID {
|
||||||
|
// There was no change,
|
||||||
|
// indicate this by unsetting
|
||||||
|
// the category name pointer.
|
||||||
|
categoryName = nil
|
||||||
} else {
|
} else {
|
||||||
// Clear existing category.
|
// Update emoji category.
|
||||||
newCategoryID = ""
|
emoji.CategoryID = category.ID
|
||||||
|
emoji.Category = category
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Emoji category was unset.
|
||||||
|
emoji.CategoryID = ""
|
||||||
|
emoji.Category = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCategoryID = emoji.CategoryID != newCategoryID
|
// Check whether any image changes were requested.
|
||||||
}
|
imageUpdated := (image != nil && image.Size > 0)
|
||||||
|
|
||||||
// Only update image
|
if !imageUpdated && categoryName != nil {
|
||||||
// if one is provided.
|
// Only updating category; only a single database update required.
|
||||||
var updateImage bool
|
|
||||||
if image != nil && image.Size != 0 {
|
|
||||||
updateImage = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if updateCategoryID && !updateImage {
|
|
||||||
// Only updating category; we only
|
|
||||||
// need to do a db update for this.
|
|
||||||
emoji.CategoryID = newCategoryID
|
|
||||||
emoji.Category = newCategory
|
|
||||||
if err := p.state.DB.UpdateEmoji(ctx, emoji, "category_id"); err != nil {
|
if err := p.state.DB.UpdateEmoji(ctx, emoji, "category_id"); err != nil {
|
||||||
err := gtserror.Newf("db error updating emoji %s: %w", emoji.ID, err)
|
err := gtserror.Newf("error updating emoji in db: %w", err)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
} else if updateImage {
|
} else if imageUpdated {
|
||||||
|
var err error
|
||||||
|
|
||||||
// Updating image and maybe categoryID.
|
// Updating image and maybe categoryID.
|
||||||
// We can do both at the same time :)
|
// We can do both at the same time :)
|
||||||
|
|
||||||
// Set data function to provided image.
|
// Simply read provided form data for emoji data source.
|
||||||
data := func(ctx context.Context) (io.ReadCloser, int64, error) {
|
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
||||||
i, err := image.Open()
|
f, err := image.Open()
|
||||||
return i, image.Size, err
|
return f, image.Size, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If necessary, include
|
// Prepare emoji model for recache from new data.
|
||||||
// update to categoryID too.
|
processing := p.media.RecacheEmoji(emoji, data)
|
||||||
var ai *media.AdditionalEmojiInfo
|
|
||||||
if updateCategoryID {
|
|
||||||
ai = &media.AdditionalEmojiInfo{
|
|
||||||
CategoryID: &newCategoryID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Begin media processing.
|
// Load to trigger update + write.
|
||||||
processingEmoji, err := p.mediaManager.PreProcessEmoji(ctx,
|
emoji, err = processing.Load(ctx)
|
||||||
data, emoji.Shortcode, emoji.ID, emoji.URI, ai, false,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := gtserror.Newf("error processing emoji: %w", err)
|
err := gtserror.Newf("error processing emoji %s: %w", emoji.Shortcode, err)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace emoji ptr with newly-processed version.
|
|
||||||
emoji, err = processingEmoji.LoadEmoji(ctx)
|
|
||||||
if err != nil {
|
|
||||||
err := gtserror.Newf("error loading emoji: %w", err)
|
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
adminEmoji, err := p.converter.EmojiToAdminAPIEmoji(ctx, emoji)
|
adminEmoji, err := p.converter.EmojiToAdminAPIEmoji(ctx, emoji)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := gtserror.Newf("error converting emoji %s to admin emoji: %w", emoji.ID, err)
|
err := gtserror.Newf("error converting emoji: %w", err)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return adminEmoji, nil
|
return adminEmoji, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createEmoji will create a new local emoji
|
||||||
|
// with the given shortcode, attached category
|
||||||
|
// name (if any) and data source function.
|
||||||
|
func (p *Processor) createEmoji(
|
||||||
|
ctx context.Context,
|
||||||
|
shortcode string,
|
||||||
|
categoryName string,
|
||||||
|
data media.DataFunc,
|
||||||
|
) (
|
||||||
|
*gtsmodel.Emoji,
|
||||||
|
gtserror.WithCode,
|
||||||
|
) {
|
||||||
|
// Validate shortcode.
|
||||||
|
if shortcode == "" {
|
||||||
|
const text = "empty shortcode name"
|
||||||
|
return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for an existing local emoji with shortcode to ensure this is new.
|
||||||
|
existing, err := p.state.DB.GetEmojiByShortcodeDomain(ctx, shortcode, "")
|
||||||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
err := gtserror.Newf("error fetching emoji from db: %w", err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
} else if existing != nil {
|
||||||
|
const text = "emoji with shortcode already exists"
|
||||||
|
return nil, gtserror.NewErrorConflict(errors.New(text), text)
|
||||||
|
}
|
||||||
|
|
||||||
|
var categoryID *string
|
||||||
|
|
||||||
|
if categoryName != "" {
|
||||||
|
// A category was provided, get / create relevant emoji category.
|
||||||
|
category, errWithCode := p.mustGetEmojiCategory(ctx, categoryName)
|
||||||
|
if errWithCode != nil {
|
||||||
|
return nil, errWithCode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set category ID for emoji.
|
||||||
|
categoryID = &category.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store to instance storage.
|
||||||
|
return p.c.StoreLocalEmoji(
|
||||||
|
ctx,
|
||||||
|
shortcode,
|
||||||
|
data,
|
||||||
|
media.AdditionalEmojiInfo{
|
||||||
|
CategoryID: categoryID,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mustGetEmojiCategory either gets an existing
|
||||||
|
// category with the given name from the database,
|
||||||
|
// or, if the category doesn't yet exist, it creates
|
||||||
|
// the category and then returns it.
|
||||||
|
func (p *Processor) mustGetEmojiCategory(
|
||||||
|
ctx context.Context,
|
||||||
|
name string,
|
||||||
|
) (
|
||||||
|
*gtsmodel.EmojiCategory,
|
||||||
|
gtserror.WithCode,
|
||||||
|
) {
|
||||||
|
// Look for an existing emoji category with name.
|
||||||
|
category, err := p.state.DB.GetEmojiCategoryByName(ctx, name)
|
||||||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
err := gtserror.Newf("error fetching emoji category from db: %w", err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if category != nil {
|
||||||
|
// We had it already.
|
||||||
|
return category, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new ID.
|
||||||
|
id := id.NewULID()
|
||||||
|
|
||||||
|
// Prepare new category for insertion.
|
||||||
|
category = >smodel.EmojiCategory{
|
||||||
|
ID: id,
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert new category into the database.
|
||||||
|
err = p.state.DB.PutEmojiCategory(ctx, category)
|
||||||
|
if err != nil {
|
||||||
|
err := gtserror.Newf("error inserting emoji category into db: %w", err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return category, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// emojisGetFilterParams builds extra
|
||||||
|
// query parameters to return as part
|
||||||
|
// of an Emojis pageable response.
|
||||||
|
//
|
||||||
|
// The returned string will look like:
|
||||||
|
//
|
||||||
|
// "filter=domain:all,enabled,shortcode:example"
|
||||||
|
func emojisGetFilterParams(
|
||||||
|
shortcode string,
|
||||||
|
domain string,
|
||||||
|
includeDisabled bool,
|
||||||
|
includeEnabled bool,
|
||||||
|
) string {
|
||||||
|
var filterBuilder strings.Builder
|
||||||
|
filterBuilder.WriteString("filter=")
|
||||||
|
|
||||||
|
switch domain {
|
||||||
|
case "", "local":
|
||||||
|
// Local emojis only.
|
||||||
|
filterBuilder.WriteString("domain:local")
|
||||||
|
|
||||||
|
case db.EmojiAllDomains:
|
||||||
|
// Local or remote.
|
||||||
|
filterBuilder.WriteString("domain:all")
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Specific domain only.
|
||||||
|
filterBuilder.WriteString("domain:" + domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
if includeDisabled != includeEnabled {
|
||||||
|
if includeDisabled {
|
||||||
|
filterBuilder.WriteString(",disabled")
|
||||||
|
}
|
||||||
|
if includeEnabled {
|
||||||
|
filterBuilder.WriteString(",enabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if shortcode != "" {
|
||||||
|
// Specific shortcode only.
|
||||||
|
filterBuilder.WriteString(",shortcode:" + shortcode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterBuilder.String()
|
||||||
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ import (
|
||||||
|
|
||||||
// MediaRefetch forces a refetch of remote emojis.
|
// MediaRefetch forces a refetch of remote emojis.
|
||||||
func (p *Processor) MediaRefetch(ctx context.Context, requestingAccount *gtsmodel.Account, domain string) gtserror.WithCode {
|
func (p *Processor) MediaRefetch(ctx context.Context, requestingAccount *gtsmodel.Account, domain string) gtserror.WithCode {
|
||||||
transport, err := p.transportController.NewTransportForUsername(ctx, requestingAccount.Username)
|
transport, err := p.transport.NewTransportForUsername(ctx, requestingAccount.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("error getting transport for user %s during media refetch request: %w", requestingAccount.Username, err)
|
err = fmt.Errorf("error getting transport for user %s during media refetch request: %w", requestingAccount.Username, err)
|
||||||
return gtserror.NewErrorInternalError(err)
|
return gtserror.NewErrorInternalError(err)
|
||||||
|
@ -36,7 +36,7 @@ func (p *Processor) MediaRefetch(ctx context.Context, requestingAccount *gtsmode
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
log.Info(ctx, "starting emoji refetch")
|
log.Info(ctx, "starting emoji refetch")
|
||||||
refetched, err := p.mediaManager.RefetchEmojis(context.Background(), domain, transport.DereferenceMedia)
|
refetched, err := p.media.RefetchEmojis(context.Background(), domain, transport.DereferenceMedia)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(ctx, "error refetching emojis: %s", err)
|
log.Errorf(ctx, "error refetching emojis: %s", err)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -20,6 +20,7 @@ package common
|
||||||
import (
|
import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||||
)
|
)
|
||||||
|
@ -29,6 +30,7 @@ import (
|
||||||
// processing subsection of the codebase.
|
// processing subsection of the codebase.
|
||||||
type Processor struct {
|
type Processor struct {
|
||||||
state *state.State
|
state *state.State
|
||||||
|
media *media.Manager
|
||||||
converter *typeutils.Converter
|
converter *typeutils.Converter
|
||||||
federator *federation.Federator
|
federator *federation.Federator
|
||||||
filter *visibility.Filter
|
filter *visibility.Filter
|
||||||
|
@ -37,12 +39,14 @@ type Processor struct {
|
||||||
// New returns a new Processor instance.
|
// New returns a new Processor instance.
|
||||||
func New(
|
func New(
|
||||||
state *state.State,
|
state *state.State,
|
||||||
|
media *media.Manager,
|
||||||
converter *typeutils.Converter,
|
converter *typeutils.Converter,
|
||||||
federator *federation.Federator,
|
federator *federation.Federator,
|
||||||
filter *visibility.Filter,
|
filter *visibility.Filter,
|
||||||
) Processor {
|
) Processor {
|
||||||
return Processor{
|
return Processor{
|
||||||
state: state,
|
state: state,
|
||||||
|
media: media,
|
||||||
converter: converter,
|
converter: converter,
|
||||||
federator: federator,
|
federator: federator,
|
||||||
filter: filter,
|
filter: filter,
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
// GoToSocial
|
||||||
|
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StoreLocalMedia is a wrapper around CreateMedia() and
|
||||||
|
// ProcessingMedia{}.Load() with appropriate error responses.
|
||||||
|
func (p *Processor) StoreLocalMedia(
|
||||||
|
ctx context.Context,
|
||||||
|
accountID string,
|
||||||
|
data media.DataFunc,
|
||||||
|
info media.AdditionalMediaInfo,
|
||||||
|
) (
|
||||||
|
*gtsmodel.MediaAttachment,
|
||||||
|
gtserror.WithCode,
|
||||||
|
) {
|
||||||
|
// Create a new processing media attachment.
|
||||||
|
processing, err := p.media.CreateMedia(ctx,
|
||||||
|
accountID,
|
||||||
|
data,
|
||||||
|
info,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
err := gtserror.Newf("error creating media: %w", err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Immediately trigger write to storage.
|
||||||
|
attachment, err := processing.Load(ctx)
|
||||||
|
if err != nil {
|
||||||
|
const text = "error processing emoji"
|
||||||
|
err := gtserror.Newf("error processing media: %w", err)
|
||||||
|
return nil, gtserror.NewErrorUnprocessableEntity(err, text)
|
||||||
|
} else if attachment.Type == gtsmodel.FileTypeUnknown {
|
||||||
|
text := fmt.Sprintf("could not process %s type media", attachment.File.ContentType)
|
||||||
|
return nil, gtserror.NewErrorUnprocessableEntity(errors.New(text), text)
|
||||||
|
}
|
||||||
|
|
||||||
|
return attachment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreLocalMedia is a wrapper around CreateMedia() and
|
||||||
|
// ProcessingMedia{}.Load() with appropriate error responses.
|
||||||
|
func (p *Processor) StoreLocalEmoji(
|
||||||
|
ctx context.Context,
|
||||||
|
shortcode string,
|
||||||
|
data media.DataFunc,
|
||||||
|
info media.AdditionalEmojiInfo,
|
||||||
|
) (
|
||||||
|
*gtsmodel.Emoji,
|
||||||
|
gtserror.WithCode,
|
||||||
|
) {
|
||||||
|
// Create a new processing emoji media.
|
||||||
|
processing, err := p.media.CreateEmoji(ctx,
|
||||||
|
shortcode,
|
||||||
|
"", // domain = "" -> local
|
||||||
|
data,
|
||||||
|
info,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
err := gtserror.Newf("error creating emoji: %w", err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Immediately write to storage.
|
||||||
|
emoji, err := processing.Load(ctx)
|
||||||
|
if err != nil {
|
||||||
|
const text = "error processing emoji"
|
||||||
|
err := gtserror.Newf("error processing emoji %s: %w", shortcode, err)
|
||||||
|
return nil, gtserror.NewErrorUnprocessableEntity(err, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
return emoji, nil
|
||||||
|
}
|
|
@ -246,9 +246,13 @@ func (p *Processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSe
|
||||||
|
|
||||||
if form.Avatar != nil && form.Avatar.Size != 0 {
|
if form.Avatar != nil && form.Avatar.Size != 0 {
|
||||||
// Process instance avatar image + description.
|
// Process instance avatar image + description.
|
||||||
avatarInfo, err := p.account.UpdateAvatar(ctx, form.Avatar, form.AvatarDescription, instanceAcc.ID)
|
avatarInfo, errWithCode := p.account.UpdateAvatar(ctx,
|
||||||
if err != nil {
|
instanceAcc,
|
||||||
return nil, gtserror.NewErrorBadRequest(err, "error processing avatar")
|
form.Avatar,
|
||||||
|
form.AvatarDescription,
|
||||||
|
)
|
||||||
|
if errWithCode != nil {
|
||||||
|
return nil, errWithCode
|
||||||
}
|
}
|
||||||
instanceAcc.AvatarMediaAttachmentID = avatarInfo.ID
|
instanceAcc.AvatarMediaAttachmentID = avatarInfo.ID
|
||||||
instanceAcc.AvatarMediaAttachment = avatarInfo
|
instanceAcc.AvatarMediaAttachment = avatarInfo
|
||||||
|
@ -264,9 +268,13 @@ func (p *Processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSe
|
||||||
|
|
||||||
if form.Header != nil && form.Header.Size != 0 {
|
if form.Header != nil && form.Header.Size != 0 {
|
||||||
// process instance header image
|
// process instance header image
|
||||||
headerInfo, err := p.account.UpdateHeader(ctx, form.Header, nil, instanceAcc.ID)
|
headerInfo, errWithCode := p.account.UpdateHeader(ctx,
|
||||||
if err != nil {
|
instanceAcc,
|
||||||
return nil, gtserror.NewErrorBadRequest(err, "error processing header")
|
form.Header,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if errWithCode != nil {
|
||||||
|
return nil, errWithCode
|
||||||
}
|
}
|
||||||
instanceAcc.HeaderMediaAttachmentID = headerInfo.ID
|
instanceAcc.HeaderMediaAttachmentID = headerInfo.ID
|
||||||
instanceAcc.HeaderMediaAttachment = headerInfo
|
instanceAcc.HeaderMediaAttachment = headerInfo
|
||||||
|
|
|
@ -30,7 +30,7 @@ import (
|
||||||
|
|
||||||
// Create creates a new media attachment belonging to the given account, using the request form.
|
// Create creates a new media attachment belonging to the given account, using the request form.
|
||||||
func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, gtserror.WithCode) {
|
func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, gtserror.WithCode) {
|
||||||
data := func(innerCtx context.Context) (io.ReadCloser, int64, error) {
|
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
||||||
f, err := form.File.Open()
|
f, err := form.File.Open()
|
||||||
return f, form.File.Size, err
|
return f, form.File.Size, err
|
||||||
}
|
}
|
||||||
|
@ -41,19 +41,18 @@ func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form
|
||||||
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// process the media attachment and load it immediately
|
// Create local media and write to instance storage.
|
||||||
media := p.mediaManager.PreProcessMedia(data, account.ID, &media.AdditionalMediaInfo{
|
attachment, errWithCode := p.c.StoreLocalMedia(ctx,
|
||||||
|
account.ID,
|
||||||
|
data,
|
||||||
|
media.AdditionalMediaInfo{
|
||||||
Description: &form.Description,
|
Description: &form.Description,
|
||||||
FocusX: &focusX,
|
FocusX: &focusX,
|
||||||
FocusY: &focusY,
|
FocusY: &focusY,
|
||||||
})
|
},
|
||||||
|
)
|
||||||
attachment, err := media.LoadAttachment(ctx)
|
if errWithCode != nil {
|
||||||
if err != nil {
|
return nil, errWithCode
|
||||||
return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
|
||||||
} else if attachment.Type == gtsmodel.FileTypeUnknown {
|
|
||||||
err = gtserror.Newf("could not process uploaded file with extension %s", attachment.File.ContentType)
|
|
||||||
return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apiAttachment, err := p.converter.AttachmentToAPIAttachment(ctx, attachment)
|
apiAttachment, err := p.converter.AttachmentToAPIAttachment(ctx, attachment)
|
||||||
|
|
|
@ -19,14 +19,14 @@ package media
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
|
@ -38,7 +38,7 @@ import (
|
||||||
// to the caller via an io.reader embedded in *apimodel.Content.
|
// to the caller via an io.reader embedded in *apimodel.Content.
|
||||||
func (p *Processor) GetFile(
|
func (p *Processor) GetFile(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
requestingAccount *gtsmodel.Account,
|
requester *gtsmodel.Account,
|
||||||
form *apimodel.GetContentRequestForm,
|
form *apimodel.GetContentRequestForm,
|
||||||
) (*apimodel.Content, gtserror.WithCode) {
|
) (*apimodel.Content, gtserror.WithCode) {
|
||||||
// parse the form fields
|
// parse the form fields
|
||||||
|
@ -69,13 +69,13 @@ func (p *Processor) GetFile(
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure the requesting account and the media account don't block each other
|
// make sure the requesting account and the media account don't block each other
|
||||||
if requestingAccount != nil {
|
if requester != nil {
|
||||||
blocked, err := p.state.DB.IsEitherBlocked(ctx, requestingAccount.ID, owningAccountID)
|
blocked, err := p.state.DB.IsEitherBlocked(ctx, requester.ID, owningAccountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block status could not be established between accounts %s and %s: %s", owningAccountID, requestingAccount.ID, err))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block status could not be established between accounts %s and %s: %s", owningAccountID, requester.ID, err))
|
||||||
}
|
}
|
||||||
if blocked {
|
if blocked {
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts %s and %s", owningAccountID, requestingAccount.ID))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts %s and %s", owningAccountID, requester.ID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,17 +83,254 @@ func (p *Processor) GetFile(
|
||||||
// so we need to take different steps depending on the media type being requested
|
// so we need to take different steps depending on the media type being requested
|
||||||
switch mediaType {
|
switch mediaType {
|
||||||
case media.TypeEmoji:
|
case media.TypeEmoji:
|
||||||
return p.getEmojiContent(ctx, wantedMediaID, owningAccountID, mediaSize)
|
return p.getEmojiContent(ctx,
|
||||||
|
owningAccountID,
|
||||||
|
wantedMediaID,
|
||||||
|
mediaSize,
|
||||||
|
)
|
||||||
case media.TypeAttachment, media.TypeHeader, media.TypeAvatar:
|
case media.TypeAttachment, media.TypeHeader, media.TypeAvatar:
|
||||||
return p.getAttachmentContent(ctx, requestingAccount, wantedMediaID, owningAccountID, mediaSize)
|
return p.getAttachmentContent(ctx,
|
||||||
|
requester,
|
||||||
|
owningAccountID,
|
||||||
|
wantedMediaID,
|
||||||
|
mediaSize,
|
||||||
|
)
|
||||||
default:
|
default:
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("media type %s not recognized", mediaType))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("media type %s not recognized", mediaType))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
func (p *Processor) getAttachmentContent(
|
||||||
UTIL FUNCTIONS
|
ctx context.Context,
|
||||||
*/
|
requester *gtsmodel.Account,
|
||||||
|
ownerID string,
|
||||||
|
mediaID string,
|
||||||
|
sizeStr media.Size,
|
||||||
|
) (
|
||||||
|
*apimodel.Content,
|
||||||
|
gtserror.WithCode,
|
||||||
|
) {
|
||||||
|
// Search for media with given ID in the database.
|
||||||
|
attach, err := p.state.DB.GetAttachmentByID(ctx, mediaID)
|
||||||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
err := gtserror.Newf("error fetching media from database: %w", err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if attach == nil {
|
||||||
|
const text = "media not found"
|
||||||
|
return nil, gtserror.NewErrorNotFound(errors.New(text), text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the 'owner' owns media.
|
||||||
|
if attach.AccountID != ownerID {
|
||||||
|
const text = "media was not owned by passed account id"
|
||||||
|
return nil, gtserror.NewErrorNotFound(errors.New(text) /* no help text! */)
|
||||||
|
}
|
||||||
|
|
||||||
|
var remoteURL *url.URL
|
||||||
|
if attach.RemoteURL != "" {
|
||||||
|
|
||||||
|
// Parse media remote URL to valid URL object.
|
||||||
|
remoteURL, err = url.Parse(attach.RemoteURL)
|
||||||
|
if err != nil {
|
||||||
|
err := gtserror.Newf("invalid media remote url %s: %w", attach.RemoteURL, err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uknown file types indicate no *locally*
|
||||||
|
// stored data we can serve. Handle separately.
|
||||||
|
if attach.Type == gtsmodel.FileTypeUnknown {
|
||||||
|
if remoteURL == nil {
|
||||||
|
err := gtserror.Newf("missing remote url for unknown type media %s: %w", attach.ID, err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is an "Unknown" file type, ie., one we
|
||||||
|
// tried to process and couldn't, or one we refused
|
||||||
|
// to process because it wasn't supported, then we
|
||||||
|
// can skip a lot of steps here by simply forwarding
|
||||||
|
// the request to the remote URL.
|
||||||
|
url := &storage.PresignedURL{
|
||||||
|
URL: remoteURL,
|
||||||
|
|
||||||
|
// We might manage to cache the media
|
||||||
|
// at some point, so set a low-ish expiry.
|
||||||
|
Expiry: time.Now().Add(2 * time.Hour),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &apimodel.Content{URL: url}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestUser string
|
||||||
|
|
||||||
|
if requester != nil {
|
||||||
|
// Set requesting acc username.
|
||||||
|
requestUser = requester.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that stored media is cached.
|
||||||
|
// (this handles local media / recaches).
|
||||||
|
attach, err = p.federator.RefreshMedia(
|
||||||
|
ctx,
|
||||||
|
requestUser,
|
||||||
|
attach,
|
||||||
|
media.AdditionalMediaInfo{},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
err := gtserror.Newf("error recaching media: %w", err)
|
||||||
|
return nil, gtserror.NewErrorNotFound(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start preparing API content model.
|
||||||
|
apiContent := &apimodel.Content{
|
||||||
|
ContentUpdated: attach.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve appropriate
|
||||||
|
// size file from storage.
|
||||||
|
switch sizeStr {
|
||||||
|
|
||||||
|
case media.SizeOriginal:
|
||||||
|
apiContent.ContentType = attach.File.ContentType
|
||||||
|
apiContent.ContentLength = int64(attach.File.FileSize)
|
||||||
|
return p.getContent(ctx,
|
||||||
|
attach.File.Path,
|
||||||
|
apiContent,
|
||||||
|
)
|
||||||
|
|
||||||
|
case media.SizeSmall:
|
||||||
|
apiContent.ContentType = attach.Thumbnail.ContentType
|
||||||
|
apiContent.ContentLength = int64(attach.Thumbnail.FileSize)
|
||||||
|
return p.getContent(ctx,
|
||||||
|
attach.Thumbnail.Path,
|
||||||
|
apiContent,
|
||||||
|
)
|
||||||
|
|
||||||
|
default:
|
||||||
|
const text = "invalid media attachment size"
|
||||||
|
return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Processor) getEmojiContent(
|
||||||
|
ctx context.Context,
|
||||||
|
|
||||||
|
ownerID string,
|
||||||
|
emojiID string,
|
||||||
|
sizeStr media.Size,
|
||||||
|
) (
|
||||||
|
*apimodel.Content,
|
||||||
|
gtserror.WithCode,
|
||||||
|
) {
|
||||||
|
// Reconstruct static emoji image URL to search for it.
|
||||||
|
// As refreshed emojis use a newly generated path ID to
|
||||||
|
// differentiate them (cache-wise) from the original.
|
||||||
|
staticURL := uris.URIForAttachment(
|
||||||
|
ownerID,
|
||||||
|
string(media.TypeEmoji),
|
||||||
|
string(media.SizeStatic),
|
||||||
|
emojiID,
|
||||||
|
"png",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Search for emoji with given static URL in the database.
|
||||||
|
emoji, err := p.state.DB.GetEmojiByStaticURL(ctx, staticURL)
|
||||||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
err := gtserror.Newf("error fetching emoji from database: %w", err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if emoji == nil {
|
||||||
|
const text = "emoji not found"
|
||||||
|
return nil, gtserror.NewErrorNotFound(errors.New(text), text)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *emoji.Disabled {
|
||||||
|
const text = "emoji has been disabled"
|
||||||
|
return nil, gtserror.NewErrorNotFound(errors.New(text), text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that stored emoji is cached.
|
||||||
|
// (this handles local emoji / recaches).
|
||||||
|
emoji, err = p.federator.RefreshEmoji(
|
||||||
|
ctx,
|
||||||
|
emoji,
|
||||||
|
media.AdditionalEmojiInfo{},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
err := gtserror.Newf("error recaching emoji: %w", err)
|
||||||
|
return nil, gtserror.NewErrorNotFound(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start preparing API content model.
|
||||||
|
apiContent := &apimodel.Content{}
|
||||||
|
|
||||||
|
// Retrieve appropriate
|
||||||
|
// size file from storage.
|
||||||
|
switch sizeStr {
|
||||||
|
|
||||||
|
case media.SizeOriginal:
|
||||||
|
apiContent.ContentType = emoji.ImageContentType
|
||||||
|
apiContent.ContentLength = int64(emoji.ImageFileSize)
|
||||||
|
return p.getContent(ctx,
|
||||||
|
emoji.ImagePath,
|
||||||
|
apiContent,
|
||||||
|
)
|
||||||
|
|
||||||
|
case media.SizeStatic:
|
||||||
|
apiContent.ContentType = emoji.ImageStaticContentType
|
||||||
|
apiContent.ContentLength = int64(emoji.ImageStaticFileSize)
|
||||||
|
return p.getContent(ctx,
|
||||||
|
emoji.ImageStaticPath,
|
||||||
|
apiContent,
|
||||||
|
)
|
||||||
|
|
||||||
|
default:
|
||||||
|
const text = "invalid media attachment size"
|
||||||
|
return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getContent performs the final file fetching of
|
||||||
|
// stored content at path in storage. This is
|
||||||
|
// populated in the apimodel.Content{} and returned.
|
||||||
|
// (note: this also handles un-proxied S3 storage).
|
||||||
|
func (p *Processor) getContent(
|
||||||
|
ctx context.Context,
|
||||||
|
path string,
|
||||||
|
content *apimodel.Content,
|
||||||
|
) (
|
||||||
|
*apimodel.Content,
|
||||||
|
gtserror.WithCode,
|
||||||
|
) {
|
||||||
|
// If running on S3 storage with proxying disabled then
|
||||||
|
// just fetch pre-signed URL instead of the content.
|
||||||
|
if url := p.state.Storage.URL(ctx, path); url != nil {
|
||||||
|
content.URL = url
|
||||||
|
return content, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch file stream for the stored media at path.
|
||||||
|
rc, err := p.state.Storage.GetStream(ctx, path)
|
||||||
|
if err != nil && !storage.IsNotFound(err) {
|
||||||
|
err := gtserror.Newf("error getting file %s from storage: %w", path, err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure found.
|
||||||
|
if rc == nil {
|
||||||
|
const text = "file not found"
|
||||||
|
return nil, gtserror.NewErrorNotFound(errors.New(text), text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return with stream.
|
||||||
|
content.Content = rc
|
||||||
|
return content, nil
|
||||||
|
}
|
||||||
|
|
||||||
func parseType(s string) (media.Type, error) {
|
func parseType(s string) (media.Type, error) {
|
||||||
switch s {
|
switch s {
|
||||||
|
@ -120,198 +357,3 @@ func parseSize(s string) (media.Size, error) {
|
||||||
}
|
}
|
||||||
return "", fmt.Errorf("%s not a recognized media.Size", s)
|
return "", fmt.Errorf("%s not a recognized media.Size", s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Processor) getAttachmentContent(ctx context.Context, requestingAccount *gtsmodel.Account, wantedMediaID string, owningAccountID string, mediaSize media.Size) (*apimodel.Content, gtserror.WithCode) {
|
|
||||||
// retrieve attachment from the database and do basic checks on it
|
|
||||||
a, err := p.state.DB.GetAttachmentByID(ctx, wantedMediaID)
|
|
||||||
if err != nil {
|
|
||||||
err = gtserror.Newf("attachment %s could not be taken from the db: %w", wantedMediaID, err)
|
|
||||||
return nil, gtserror.NewErrorNotFound(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if a.AccountID != owningAccountID {
|
|
||||||
err = gtserror.Newf("attachment %s is not owned by %s", wantedMediaID, owningAccountID)
|
|
||||||
return nil, gtserror.NewErrorNotFound(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this is an "Unknown" file type, ie., one we
|
|
||||||
// tried to process and couldn't, or one we refused
|
|
||||||
// to process because it wasn't supported, then we
|
|
||||||
// can skip a lot of steps here by simply forwarding
|
|
||||||
// the request to the remote URL.
|
|
||||||
if a.Type == gtsmodel.FileTypeUnknown {
|
|
||||||
remoteURL, err := url.Parse(a.RemoteURL)
|
|
||||||
if err != nil {
|
|
||||||
err = gtserror.Newf("error parsing remote URL of 'Unknown'-type attachment for redirection: %w", err)
|
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
url := &storage.PresignedURL{
|
|
||||||
URL: remoteURL,
|
|
||||||
// We might manage to cache the media
|
|
||||||
// at some point, so set a low-ish expiry.
|
|
||||||
Expiry: time.Now().Add(2 * time.Hour),
|
|
||||||
}
|
|
||||||
|
|
||||||
return &apimodel.Content{URL: url}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !*a.Cached {
|
|
||||||
// if we don't have it cached, then we can assume two things:
|
|
||||||
// 1. this is remote media, since local media should never be uncached
|
|
||||||
// 2. we need to fetch it again using a transport and the media manager
|
|
||||||
remoteMediaIRI, err := url.Parse(a.RemoteURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error parsing remote media iri %s: %w", a.RemoteURL, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// use an empty string as requestingUsername to use the instance account, unless the request for this
|
|
||||||
// media has been http signed, then use the requesting account to make the request to remote server
|
|
||||||
var requestingUsername string
|
|
||||||
if requestingAccount != nil {
|
|
||||||
requestingUsername = requestingAccount.Username
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pour one out for tobi's original streamed recache
|
|
||||||
// (streaming data both to the client and storage).
|
|
||||||
// Gone and forever missed <3
|
|
||||||
//
|
|
||||||
// [
|
|
||||||
// the reason it was removed was because a slow
|
|
||||||
// client connection could hold open a storage
|
|
||||||
// recache operation -> holding open a media worker.
|
|
||||||
// ]
|
|
||||||
|
|
||||||
dataFn := func(ctx context.Context) (io.ReadCloser, int64, error) {
|
|
||||||
t, err := p.transportController.NewTransportForUsername(ctx, requestingUsername)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
return t.DereferenceMedia(gtscontext.SetFastFail(ctx), remoteMediaIRI)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start recaching this media with the prepared data function.
|
|
||||||
processingMedia, err := p.mediaManager.PreProcessMediaRecache(ctx, dataFn, wantedMediaID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error recaching media: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load attachment and block until complete
|
|
||||||
a, err = processingMedia.LoadAttachment(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error loading recached attachment: %w", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
storagePath string
|
|
||||||
attachmentContent = &apimodel.Content{
|
|
||||||
ContentUpdated: a.UpdatedAt,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// get file information from the attachment depending on the requested media size
|
|
||||||
switch mediaSize {
|
|
||||||
case media.SizeOriginal:
|
|
||||||
attachmentContent.ContentType = a.File.ContentType
|
|
||||||
attachmentContent.ContentLength = int64(a.File.FileSize)
|
|
||||||
storagePath = a.File.Path
|
|
||||||
case media.SizeSmall:
|
|
||||||
attachmentContent.ContentType = a.Thumbnail.ContentType
|
|
||||||
attachmentContent.ContentLength = int64(a.Thumbnail.FileSize)
|
|
||||||
storagePath = a.Thumbnail.Path
|
|
||||||
default:
|
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not recognized for attachment", mediaSize))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... so now we can safely return it
|
|
||||||
return p.retrieveFromStorage(ctx, storagePath, attachmentContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Processor) getEmojiContent(ctx context.Context, fileName string, owningAccountID string, emojiSize media.Size) (*apimodel.Content, gtserror.WithCode) {
|
|
||||||
emojiContent := &apimodel.Content{}
|
|
||||||
var storagePath string
|
|
||||||
|
|
||||||
// reconstruct the static emoji image url -- reason
|
|
||||||
// for using the static URL rather than full size url
|
|
||||||
// is that static emojis are always encoded as png,
|
|
||||||
// so this is more reliable than using full size url
|
|
||||||
imageStaticURL := uris.URIForAttachment(
|
|
||||||
owningAccountID,
|
|
||||||
string(media.TypeEmoji),
|
|
||||||
string(media.SizeStatic),
|
|
||||||
fileName,
|
|
||||||
"png",
|
|
||||||
)
|
|
||||||
|
|
||||||
e, err := p.state.DB.GetEmojiByStaticURL(ctx, imageStaticURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s could not be taken from the db: %w", fileName, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if *e.Disabled {
|
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s has been disabled", fileName))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !*e.Cached {
|
|
||||||
// if we don't have it cached, then we can assume two things:
|
|
||||||
// 1. this is remote emoji, since local emoji should never be uncached
|
|
||||||
// 2. we need to fetch it again using a transport and the media manager
|
|
||||||
remoteURL, err := url.Parse(e.ImageRemoteURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error parsing remote emoji iri %s: %w", e.ImageRemoteURL, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
dataFn := func(ctx context.Context) (io.ReadCloser, int64, error) {
|
|
||||||
t, err := p.transportController.NewTransportForUsername(ctx, "")
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
return t.DereferenceMedia(gtscontext.SetFastFail(ctx), remoteURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start recaching this emoji with the prepared data function.
|
|
||||||
processingEmoji, err := p.mediaManager.PreProcessEmojiRecache(ctx, dataFn, e.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error recaching emoji: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load attachment and block until complete
|
|
||||||
e, err = processingEmoji.LoadEmoji(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error loading recached emoji: %w", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch emojiSize {
|
|
||||||
case media.SizeOriginal:
|
|
||||||
emojiContent.ContentType = e.ImageContentType
|
|
||||||
emojiContent.ContentLength = int64(e.ImageFileSize)
|
|
||||||
storagePath = e.ImagePath
|
|
||||||
case media.SizeStatic:
|
|
||||||
emojiContent.ContentType = e.ImageStaticContentType
|
|
||||||
emojiContent.ContentLength = int64(e.ImageStaticFileSize)
|
|
||||||
storagePath = e.ImageStaticPath
|
|
||||||
default:
|
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not recognized for emoji", emojiSize))
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.retrieveFromStorage(ctx, storagePath, emojiContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Processor) retrieveFromStorage(ctx context.Context, storagePath string, content *apimodel.Content) (*apimodel.Content, gtserror.WithCode) {
|
|
||||||
// If running on S3 storage with proxying disabled then
|
|
||||||
// just fetch a pre-signed URL instead of serving the content.
|
|
||||||
if url := p.state.Storage.URL(ctx, storagePath); url != nil {
|
|
||||||
content.URL = url
|
|
||||||
return content, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
reader, err := p.state.Storage.GetStream(ctx, storagePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error retrieving from storage: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
content.Content = reader
|
|
||||||
return content, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -18,24 +18,39 @@
|
||||||
package media
|
package media
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Processor struct {
|
type Processor struct {
|
||||||
|
// common processor logic
|
||||||
|
c *common.Processor
|
||||||
|
|
||||||
state *state.State
|
state *state.State
|
||||||
converter *typeutils.Converter
|
converter *typeutils.Converter
|
||||||
|
federator *federation.Federator
|
||||||
mediaManager *media.Manager
|
mediaManager *media.Manager
|
||||||
transportController transport.Controller
|
transportController transport.Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new media processor.
|
// New returns a new media processor.
|
||||||
func New(state *state.State, converter *typeutils.Converter, mediaManager *media.Manager, transportController transport.Controller) Processor {
|
func New(
|
||||||
|
common *common.Processor,
|
||||||
|
state *state.State,
|
||||||
|
converter *typeutils.Converter,
|
||||||
|
federator *federation.Federator,
|
||||||
|
mediaManager *media.Manager,
|
||||||
|
transportController transport.Controller,
|
||||||
|
) Processor {
|
||||||
return Processor{
|
return Processor{
|
||||||
|
c: common,
|
||||||
state: state,
|
state: state,
|
||||||
converter: converter,
|
converter: converter,
|
||||||
|
federator: federator,
|
||||||
mediaManager: mediaManager,
|
mediaManager: mediaManager,
|
||||||
transportController: transportController,
|
transportController: transportController,
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,8 +20,10 @@ package media_test
|
||||||
import (
|
import (
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
|
||||||
mediaprocessing "github.com/superseriousbusiness/gotosocial/internal/processing/media"
|
mediaprocessing "github.com/superseriousbusiness/gotosocial/internal/processing/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/storage"
|
"github.com/superseriousbusiness/gotosocial/internal/storage"
|
||||||
|
@ -78,7 +80,12 @@ func (suite *MediaStandardTestSuite) SetupTest() {
|
||||||
suite.state.Storage = suite.storage
|
suite.state.Storage = suite.storage
|
||||||
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
|
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
|
||||||
suite.transportController = testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../testrig/media"))
|
suite.transportController = testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../testrig/media"))
|
||||||
suite.mediaProcessor = mediaprocessing.New(&suite.state, suite.tc, suite.mediaManager, suite.transportController)
|
|
||||||
|
federator := testrig.NewTestFederator(&suite.state, suite.transportController, suite.mediaManager)
|
||||||
|
filter := visibility.NewFilter(&suite.state)
|
||||||
|
common := common.New(&suite.state, suite.mediaManager, suite.tc, federator, filter)
|
||||||
|
|
||||||
|
suite.mediaProcessor = mediaprocessing.New(&common, &suite.state, suite.tc, federator, suite.mediaManager, suite.transportController)
|
||||||
testrig.StandardDBSetup(suite.db, nil)
|
testrig.StandardDBSetup(suite.db, nil)
|
||||||
testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
|
testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@ func (suite *PollTestSuite) SetupTest() {
|
||||||
mediaMgr := media.NewManager(&suite.state)
|
mediaMgr := media.NewManager(&suite.state)
|
||||||
federator := testrig.NewTestFederator(&suite.state, controller, mediaMgr)
|
federator := testrig.NewTestFederator(&suite.state, controller, mediaMgr)
|
||||||
suite.filter = visibility.NewFilter(&suite.state)
|
suite.filter = visibility.NewFilter(&suite.state)
|
||||||
common := common.New(&suite.state, converter, federator, suite.filter)
|
common := common.New(&suite.state, mediaMgr, converter, federator, suite.filter)
|
||||||
suite.polls = polls.New(&common, &suite.state, converter)
|
suite.polls = polls.New(&common, &suite.state, converter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -179,15 +179,15 @@ func NewProcessor(
|
||||||
//
|
//
|
||||||
// Start with sub processors that will
|
// Start with sub processors that will
|
||||||
// be required by the workers processor.
|
// be required by the workers processor.
|
||||||
common := common.New(state, converter, federator, filter)
|
common := common.New(state, mediaManager, converter, federator, filter)
|
||||||
processor.account = account.New(&common, state, converter, mediaManager, federator, filter, parseMentionFunc)
|
processor.account = account.New(&common, state, converter, mediaManager, federator, filter, parseMentionFunc)
|
||||||
processor.media = media.New(state, converter, mediaManager, federator.TransportController())
|
processor.media = media.New(&common, state, converter, federator, mediaManager, federator.TransportController())
|
||||||
processor.stream = stream.New(state, oauthServer)
|
processor.stream = stream.New(state, oauthServer)
|
||||||
|
|
||||||
// Instantiate the rest of the sub
|
// Instantiate the rest of the sub
|
||||||
// processors + pin them to this struct.
|
// processors + pin them to this struct.
|
||||||
processor.account = account.New(&common, state, converter, mediaManager, federator, filter, parseMentionFunc)
|
processor.account = account.New(&common, state, converter, mediaManager, federator, filter, parseMentionFunc)
|
||||||
processor.admin = admin.New(state, cleaner, converter, mediaManager, federator.TransportController(), emailSender)
|
processor.admin = admin.New(&common, state, cleaner, federator, converter, mediaManager, federator.TransportController(), emailSender)
|
||||||
processor.fedi = fedi.New(state, &common, converter, federator, filter)
|
processor.fedi = fedi.New(state, &common, converter, federator, filter)
|
||||||
processor.filtersv1 = filtersv1.New(state, converter, &processor.stream)
|
processor.filtersv1 = filtersv1.New(state, converter, &processor.stream)
|
||||||
processor.filtersv2 = filtersv2.New(state, converter, &processor.stream)
|
processor.filtersv2 = filtersv2.New(state, converter, &processor.stream)
|
||||||
|
|
|
@ -96,7 +96,7 @@ func (suite *StatusStandardTestSuite) SetupTest() {
|
||||||
suite.typeConverter,
|
suite.typeConverter,
|
||||||
)
|
)
|
||||||
|
|
||||||
common := common.New(&suite.state, suite.typeConverter, suite.federator, filter)
|
common := common.New(&suite.state, suite.mediaManager, suite.typeConverter, suite.federator, filter)
|
||||||
polls := polls.New(&common, &suite.state, suite.typeConverter)
|
polls := polls.New(&common, &suite.state, suite.typeConverter)
|
||||||
suite.status = status.New(&suite.state, &common, &polls, suite.federator, suite.typeConverter, filter, processing.GetParseMentionFunc(&suite.state, suite.federator))
|
suite.status = status.New(&suite.state, &common, &polls, suite.federator, suite.typeConverter, filter, processing.GetParseMentionFunc(&suite.state, suite.federator))
|
||||||
|
|
||||||
|
|
|
@ -95,7 +95,7 @@ func (d *Driver) PutStream(ctx context.Context, key string, r io.Reader) (int64,
|
||||||
return d.Storage.WriteStream(ctx, key, r)
|
return d.Storage.WriteStream(ctx, key, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove attempts to remove the supplied key (and corresponding value) from storage.
|
// Delete attempts to remove the supplied key (and corresponding value) from storage.
|
||||||
func (d *Driver) Delete(ctx context.Context, key string) error {
|
func (d *Driver) Delete(ctx context.Context, key string) error {
|
||||||
return d.Storage.Remove(ctx, key)
|
return d.Storage.Remove(ctx, key)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1051,7 +1051,7 @@ func (c *Converter) EmojiToAS(ctx context.Context, e *gtsmodel.Emoji) (vocab.Too
|
||||||
emoji.SetActivityStreamsIcon(iconProperty)
|
emoji.SetActivityStreamsIcon(iconProperty)
|
||||||
|
|
||||||
updatedProp := streams.NewActivityStreamsUpdatedProperty()
|
updatedProp := streams.NewActivityStreamsUpdatedProperty()
|
||||||
updatedProp.Set(e.ImageUpdatedAt)
|
updatedProp.Set(e.UpdatedAt)
|
||||||
emoji.SetActivityStreamsUpdated(updatedProp)
|
emoji.SetActivityStreamsUpdated(updatedProp)
|
||||||
|
|
||||||
return emoji, nil
|
return emoji, nil
|
||||||
|
|
|
@ -49,10 +49,6 @@ type Workers struct {
|
||||||
// for asynchronous dereferencer jobs.
|
// for asynchronous dereferencer jobs.
|
||||||
Dereference FnWorkerPool
|
Dereference FnWorkerPool
|
||||||
|
|
||||||
// Media provides a worker pool for
|
|
||||||
// asynchronous media processing jobs.
|
|
||||||
Media FnWorkerPool
|
|
||||||
|
|
||||||
// prevent pass-by-value.
|
// prevent pass-by-value.
|
||||||
_ nocopy
|
_ nocopy
|
||||||
}
|
}
|
||||||
|
@ -84,10 +80,6 @@ func (w *Workers) Start() {
|
||||||
n = 4 * maxprocs
|
n = 4 * maxprocs
|
||||||
w.Dereference.Start(n)
|
w.Dereference.Start(n)
|
||||||
log.Infof(nil, "started %d dereference workers", n)
|
log.Infof(nil, "started %d dereference workers", n)
|
||||||
|
|
||||||
n = 8 * maxprocs
|
|
||||||
w.Media.Start(n)
|
|
||||||
log.Infof(nil, "started %d media workers", n)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop will stop all of the contained worker pools (and global scheduler).
|
// Stop will stop all of the contained worker pools (and global scheduler).
|
||||||
|
@ -105,9 +97,6 @@ func (w *Workers) Stop() {
|
||||||
|
|
||||||
w.Dereference.Stop()
|
w.Dereference.Stop()
|
||||||
log.Info(nil, "stopped dereference workers")
|
log.Info(nil, "stopped dereference workers")
|
||||||
|
|
||||||
w.Media.Stop()
|
|
||||||
log.Info(nil, "stopped media workers")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// nocopy when embedded will signal linter to
|
// nocopy when embedded will signal linter to
|
||||||
|
|
|
@ -739,13 +739,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||||
Path: "01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg",
|
Path: "01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg",
|
||||||
ContentType: "image/jpeg",
|
ContentType: "image/jpeg",
|
||||||
FileSize: 62529,
|
FileSize: 62529,
|
||||||
UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
|
|
||||||
},
|
},
|
||||||
Thumbnail: gtsmodel.Thumbnail{
|
Thumbnail: gtsmodel.Thumbnail{
|
||||||
Path: "01F8MH17FWEB39HZJ76B6VXSKF/attachment/small/01F8MH6NEM8D7527KZAECTCR76.jpg",
|
Path: "01F8MH17FWEB39HZJ76B6VXSKF/attachment/small/01F8MH6NEM8D7527KZAECTCR76.jpg",
|
||||||
ContentType: "image/jpeg",
|
ContentType: "image/jpeg",
|
||||||
FileSize: 6872,
|
FileSize: 6872,
|
||||||
UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
|
|
||||||
URL: "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/small/01F8MH6NEM8D7527KZAECTCR76.jpg",
|
URL: "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/small/01F8MH6NEM8D7527KZAECTCR76.jpg",
|
||||||
RemoteURL: "",
|
RemoteURL: "",
|
||||||
},
|
},
|
||||||
|
@ -788,13 +786,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||||
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01F8MH7TDVANYKWVE8VVKFPJTJ.gif",
|
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01F8MH7TDVANYKWVE8VVKFPJTJ.gif",
|
||||||
ContentType: "image/gif",
|
ContentType: "image/gif",
|
||||||
FileSize: 1109138,
|
FileSize: 1109138,
|
||||||
UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
|
|
||||||
},
|
},
|
||||||
Thumbnail: gtsmodel.Thumbnail{
|
Thumbnail: gtsmodel.Thumbnail{
|
||||||
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01F8MH7TDVANYKWVE8VVKFPJTJ.jpg",
|
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01F8MH7TDVANYKWVE8VVKFPJTJ.jpg",
|
||||||
ContentType: "image/jpeg",
|
ContentType: "image/jpeg",
|
||||||
FileSize: 8803,
|
FileSize: 8803,
|
||||||
UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
|
|
||||||
URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01F8MH7TDVANYKWVE8VVKFPJTJ.jpg",
|
URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01F8MH7TDVANYKWVE8VVKFPJTJ.jpg",
|
||||||
RemoteURL: "",
|
RemoteURL: "",
|
||||||
},
|
},
|
||||||
|
@ -840,13 +836,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||||
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01CDR64G398ADCHXK08WWTHEZ5.gif",
|
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01CDR64G398ADCHXK08WWTHEZ5.gif",
|
||||||
ContentType: "video/mp4",
|
ContentType: "video/mp4",
|
||||||
FileSize: 2273532,
|
FileSize: 2273532,
|
||||||
UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
|
|
||||||
},
|
},
|
||||||
Thumbnail: gtsmodel.Thumbnail{
|
Thumbnail: gtsmodel.Thumbnail{
|
||||||
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01CDR64G398ADCHXK08WWTHEZ5.jpg",
|
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01CDR64G398ADCHXK08WWTHEZ5.jpg",
|
||||||
ContentType: "image/jpeg",
|
ContentType: "image/jpeg",
|
||||||
FileSize: 5272,
|
FileSize: 5272,
|
||||||
UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
|
|
||||||
URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01CDR64G398ADCHXK08WWTHEZ5.jpg",
|
URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01CDR64G398ADCHXK08WWTHEZ5.jpg",
|
||||||
RemoteURL: "",
|
RemoteURL: "",
|
||||||
},
|
},
|
||||||
|
@ -889,13 +883,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||||
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01F8MH8RMYQ6MSNY3JM2XT1CQ5.jpg",
|
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01F8MH8RMYQ6MSNY3JM2XT1CQ5.jpg",
|
||||||
ContentType: "image/jpeg",
|
ContentType: "image/jpeg",
|
||||||
FileSize: 27759,
|
FileSize: 27759,
|
||||||
UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
|
|
||||||
},
|
},
|
||||||
Thumbnail: gtsmodel.Thumbnail{
|
Thumbnail: gtsmodel.Thumbnail{
|
||||||
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01F8MH8RMYQ6MSNY3JM2XT1CQ5.jpg",
|
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01F8MH8RMYQ6MSNY3JM2XT1CQ5.jpg",
|
||||||
ContentType: "image/jpeg",
|
ContentType: "image/jpeg",
|
||||||
FileSize: 6177,
|
FileSize: 6177,
|
||||||
UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
|
|
||||||
URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01F8MH8RMYQ6MSNY3JM2XT1CQ5.jpg",
|
URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01F8MH8RMYQ6MSNY3JM2XT1CQ5.jpg",
|
||||||
RemoteURL: "",
|
RemoteURL: "",
|
||||||
},
|
},
|
||||||
|
@ -938,13 +930,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||||
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||||
ContentType: "image/jpeg",
|
ContentType: "image/jpeg",
|
||||||
FileSize: 457680,
|
FileSize: 457680,
|
||||||
UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
|
|
||||||
},
|
},
|
||||||
Thumbnail: gtsmodel.Thumbnail{
|
Thumbnail: gtsmodel.Thumbnail{
|
||||||
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||||
ContentType: "image/jpeg",
|
ContentType: "image/jpeg",
|
||||||
FileSize: 15374,
|
FileSize: 15374,
|
||||||
UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
|
|
||||||
URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||||
RemoteURL: "",
|
RemoteURL: "",
|
||||||
},
|
},
|
||||||
|
@ -987,13 +977,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||||
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||||
ContentType: "image/jpeg",
|
ContentType: "image/jpeg",
|
||||||
FileSize: 517226,
|
FileSize: 517226,
|
||||||
UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
|
|
||||||
},
|
},
|
||||||
Thumbnail: gtsmodel.Thumbnail{
|
Thumbnail: gtsmodel.Thumbnail{
|
||||||
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
Path: "01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||||
ContentType: "image/jpeg",
|
ContentType: "image/jpeg",
|
||||||
FileSize: 42308,
|
FileSize: 42308,
|
||||||
UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
|
|
||||||
URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||||
RemoteURL: "",
|
RemoteURL: "",
|
||||||
},
|
},
|
||||||
|
@ -1036,13 +1024,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||||
Path: "01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
|
Path: "01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
|
||||||
ContentType: "image/jpeg",
|
ContentType: "image/jpeg",
|
||||||
FileSize: 19310,
|
FileSize: 19310,
|
||||||
UpdatedAt: TimeMustParse("2021-09-20T12:40:37+02:00"),
|
|
||||||
},
|
},
|
||||||
Thumbnail: gtsmodel.Thumbnail{
|
Thumbnail: gtsmodel.Thumbnail{
|
||||||
Path: "01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
|
Path: "01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
|
||||||
ContentType: "image/jpeg",
|
ContentType: "image/jpeg",
|
||||||
FileSize: 19312,
|
FileSize: 19312,
|
||||||
UpdatedAt: TimeMustParse("2021-09-20T12:40:37+02:00"),
|
|
||||||
URL: "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
|
URL: "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/small/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
|
||||||
RemoteURL: "http://fossbros-anonymous.io/attachments/small/a499f55b-2d1e-4acd-98d2-1ac2ba6d79b9.jpg",
|
RemoteURL: "http://fossbros-anonymous.io/attachments/small/a499f55b-2d1e-4acd-98d2-1ac2ba6d79b9.jpg",
|
||||||
},
|
},
|
||||||
|
@ -1085,13 +1071,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||||
Path: "062G5WYKY35KKD12EMSM3F8PJ8/attachment/original/01PFPMWK2FF0D9WMHEJHR07C3R.jpg",
|
Path: "062G5WYKY35KKD12EMSM3F8PJ8/attachment/original/01PFPMWK2FF0D9WMHEJHR07C3R.jpg",
|
||||||
ContentType: "image/jpeg",
|
ContentType: "image/jpeg",
|
||||||
FileSize: 19310,
|
FileSize: 19310,
|
||||||
UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
|
|
||||||
},
|
},
|
||||||
Thumbnail: gtsmodel.Thumbnail{
|
Thumbnail: gtsmodel.Thumbnail{
|
||||||
Path: "062G5WYKY35KKD12EMSM3F8PJ8/attachment/small/01PFPMWK2FF0D9WMHEJHR07C3R.jpg",
|
Path: "062G5WYKY35KKD12EMSM3F8PJ8/attachment/small/01PFPMWK2FF0D9WMHEJHR07C3R.jpg",
|
||||||
ContentType: "image/jpeg",
|
ContentType: "image/jpeg",
|
||||||
FileSize: 20395,
|
FileSize: 20395,
|
||||||
UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
|
|
||||||
URL: "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/small/01PFPMWK2FF0D9WMHEJHR07C3R.jpg",
|
URL: "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/small/01PFPMWK2FF0D9WMHEJHR07C3R.jpg",
|
||||||
RemoteURL: "http://fossbros-anonymous.io/attachments/small/a499f55b-2d1e-4acd-98d2-1ac2ba6d79b9.jpg",
|
RemoteURL: "http://fossbros-anonymous.io/attachments/small/a499f55b-2d1e-4acd-98d2-1ac2ba6d79b9.jpg",
|
||||||
},
|
},
|
||||||
|
@ -1133,13 +1117,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||||
Path: "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE7Y3C432WRSNS10EZM86SA5.jpg",
|
Path: "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE7Y3C432WRSNS10EZM86SA5.jpg",
|
||||||
ContentType: "image/jpg",
|
ContentType: "image/jpg",
|
||||||
FileSize: 5450054,
|
FileSize: 5450054,
|
||||||
UpdatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"),
|
|
||||||
},
|
},
|
||||||
Thumbnail: gtsmodel.Thumbnail{
|
Thumbnail: gtsmodel.Thumbnail{
|
||||||
Path: "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE7Y3C432WRSNS10EZM86SA5.jpg",
|
Path: "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE7Y3C432WRSNS10EZM86SA5.jpg",
|
||||||
ContentType: "image/jpeg",
|
ContentType: "image/jpeg",
|
||||||
FileSize: 50820,
|
FileSize: 50820,
|
||||||
UpdatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"),
|
|
||||||
URL: "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE7Y3C432WRSNS10EZM86SA5.jpg",
|
URL: "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE7Y3C432WRSNS10EZM86SA5.jpg",
|
||||||
},
|
},
|
||||||
Avatar: util.Ptr(false),
|
Avatar: util.Ptr(false),
|
||||||
|
@ -1163,13 +1145,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||||
Path: "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE7ZFX9GKA5ZZVD4FACABSS9.svg",
|
Path: "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE7ZFX9GKA5ZZVD4FACABSS9.svg",
|
||||||
ContentType: "image/svg",
|
ContentType: "image/svg",
|
||||||
FileSize: 147819,
|
FileSize: 147819,
|
||||||
UpdatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"),
|
|
||||||
},
|
},
|
||||||
Thumbnail: gtsmodel.Thumbnail{
|
Thumbnail: gtsmodel.Thumbnail{
|
||||||
Path: "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE7ZFX9GKA5ZZVD4FACABSS9.jpg",
|
Path: "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE7ZFX9GKA5ZZVD4FACABSS9.jpg",
|
||||||
ContentType: "image/jpeg",
|
ContentType: "image/jpeg",
|
||||||
FileSize: 0,
|
FileSize: 0,
|
||||||
UpdatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"),
|
|
||||||
URL: "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE7ZFX9GKA5ZZVD4FACABSS9.jpg",
|
URL: "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE7ZFX9GKA5ZZVD4FACABSS9.jpg",
|
||||||
},
|
},
|
||||||
Avatar: util.Ptr(false),
|
Avatar: util.Ptr(false),
|
||||||
|
@ -1193,13 +1173,11 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
||||||
Path: "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE88YG74PVAB81PX2XA9F3FG.mp3",
|
Path: "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE88YG74PVAB81PX2XA9F3FG.mp3",
|
||||||
ContentType: "audio/mpeg",
|
ContentType: "audio/mpeg",
|
||||||
FileSize: 147819,
|
FileSize: 147819,
|
||||||
UpdatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"),
|
|
||||||
},
|
},
|
||||||
Thumbnail: gtsmodel.Thumbnail{
|
Thumbnail: gtsmodel.Thumbnail{
|
||||||
Path: "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE88YG74PVAB81PX2XA9F3FG.jpg",
|
Path: "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE88YG74PVAB81PX2XA9F3FG.jpg",
|
||||||
ContentType: "image/jpeg",
|
ContentType: "image/jpeg",
|
||||||
FileSize: 0,
|
FileSize: 0,
|
||||||
UpdatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"),
|
|
||||||
URL: "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE88YG74PVAB81PX2XA9F3FG.jpg",
|
URL: "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE88YG74PVAB81PX2XA9F3FG.jpg",
|
||||||
},
|
},
|
||||||
Avatar: util.Ptr(false),
|
Avatar: util.Ptr(false),
|
||||||
|
@ -1228,7 +1206,6 @@ func NewTestEmojis() map[string]*gtsmodel.Emoji {
|
||||||
ImageStaticContentType: "image/png",
|
ImageStaticContentType: "image/png",
|
||||||
ImageFileSize: 36702,
|
ImageFileSize: 36702,
|
||||||
ImageStaticFileSize: 10413,
|
ImageStaticFileSize: 10413,
|
||||||
ImageUpdatedAt: TimeMustParse("2021-09-20T12:40:37+02:00"),
|
|
||||||
Disabled: util.Ptr(false),
|
Disabled: util.Ptr(false),
|
||||||
URI: "http://localhost:8080/emoji/01F8MH9H8E4VG3KDYJR9EGPXCQ",
|
URI: "http://localhost:8080/emoji/01F8MH9H8E4VG3KDYJR9EGPXCQ",
|
||||||
VisibleInPicker: util.Ptr(true),
|
VisibleInPicker: util.Ptr(true),
|
||||||
|
@ -1251,7 +1228,6 @@ func NewTestEmojis() map[string]*gtsmodel.Emoji {
|
||||||
ImageStaticContentType: "image/png",
|
ImageStaticContentType: "image/png",
|
||||||
ImageFileSize: 10889,
|
ImageFileSize: 10889,
|
||||||
ImageStaticFileSize: 10808,
|
ImageStaticFileSize: 10808,
|
||||||
ImageUpdatedAt: TimeMustParse("2020-03-18T13:12:00+01:00"),
|
|
||||||
Disabled: util.Ptr(false),
|
Disabled: util.Ptr(false),
|
||||||
URI: "http://fossbros-anonymous.io/emoji/01GD5KP5CQEE1R3X43Y1EHS2CW",
|
URI: "http://fossbros-anonymous.io/emoji/01GD5KP5CQEE1R3X43Y1EHS2CW",
|
||||||
VisibleInPicker: util.Ptr(false),
|
VisibleInPicker: util.Ptr(false),
|
||||||
|
|
|
@ -82,7 +82,6 @@ func StartWorkers(state *state.State, processor *workers.Processor) {
|
||||||
state.Workers.Client.Start(1)
|
state.Workers.Client.Start(1)
|
||||||
state.Workers.Federator.Start(1)
|
state.Workers.Federator.Start(1)
|
||||||
state.Workers.Dereference.Start(1)
|
state.Workers.Dereference.Start(1)
|
||||||
state.Workers.Media.Start(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func StopWorkers(state *state.State) {
|
func StopWorkers(state *state.State) {
|
||||||
|
@ -90,7 +89,6 @@ func StopWorkers(state *state.State) {
|
||||||
state.Workers.Client.Stop()
|
state.Workers.Client.Stop()
|
||||||
state.Workers.Federator.Stop()
|
state.Workers.Federator.Stop()
|
||||||
state.Workers.Dereference.Stop()
|
state.Workers.Dereference.Stop()
|
||||||
state.Workers.Media.Stop()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartTimelines(state *state.State, filter *visibility.Filter, converter *typeutils.Converter) {
|
func StartTimelines(state *state.State, filter *visibility.Filter, converter *typeutils.Converter) {
|
||||||
|
|
Loading…
Reference in New Issue