mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-24 01:11:00 +00:00
[chore] media pipeline improvements (#3110)
* don't set emoji / media image paths on failed download, migrate FileType from string to integer * fix incorrect uses of util.PtrOr, fix returned frontend media * fix migration not setting arguments correctly in where clause * fix not providing default with not null column * whoops * ensure a default gets set for media attachment file type * remove the exclusive flag from writing files in disk storage * rename PtrOr -> PtrOrZero, and rename PtrValueOr -> PtrOrValue to match * slight wording changes * use singular / plural word forms (no parentheses), is better for screen readers * update testmodels with unknown media type to have unset file details, update attachment focus handling converting to frontend, update tests * store first instance in ffmpeg wasm pool, fill remaining with closed instances
This commit is contained in:
parent
0aadc2db2a
commit
72ba5666a6
29 changed files with 669 additions and 399 deletions
|
@ -24,6 +24,7 @@ import (
|
|||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"codeberg.org/gruf/go-logger/v2/level"
|
||||
"codeberg.org/gruf/go-storage/memory"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db/bundb"
|
||||
|
@ -40,6 +41,8 @@ func main() {
|
|||
ctx, cncl := signal.NotifyContext(ctx, syscall.SIGTERM, syscall.SIGINT)
|
||||
defer cncl()
|
||||
|
||||
log.SetLevel(level.INFO)
|
||||
|
||||
if len(os.Args) != 3 {
|
||||
log.Panic(ctx, "Usage: go run ./cmd/process-emoji <input-file> <output-static>")
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"codeberg.org/gruf/go-logger/v2/level"
|
||||
"codeberg.org/gruf/go-storage/memory"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db/bundb"
|
||||
|
@ -39,6 +40,8 @@ func main() {
|
|||
ctx, cncl := signal.NotifyContext(ctx, syscall.SIGTERM, syscall.SIGINT)
|
||||
defer cncl()
|
||||
|
||||
log.SetLevel(level.INFO)
|
||||
|
||||
if len(os.Args) != 4 {
|
||||
log.Panic(ctx, "Usage: go run ./cmd/process-media <input-file> <output-processed> <output-thumbnail>")
|
||||
}
|
||||
|
|
|
@ -138,7 +138,7 @@ func (m *Module) AccountMutePOSTHandler(c *gin.Context) {
|
|||
|
||||
func normalizeCreateUpdateMute(form *apimodel.UserMuteCreateUpdateRequest) error {
|
||||
// Apply defaults for missing fields.
|
||||
form.Notifications = util.Ptr(util.PtrValueOr(form.Notifications, false))
|
||||
form.Notifications = util.Ptr(util.PtrOrValue(form.Notifications, false))
|
||||
|
||||
// Normalize mute duration if necessary.
|
||||
// If we parsed this as JSON, expires_in
|
||||
|
|
|
@ -40,8 +40,8 @@ func validateNormalizeCreateUpdateFilter(form *model.FilterCreateUpdateRequestV1
|
|||
}
|
||||
|
||||
// Apply defaults for missing fields.
|
||||
form.WholeWord = util.Ptr(util.PtrValueOr(form.WholeWord, false))
|
||||
form.Irreversible = util.Ptr(util.PtrValueOr(form.Irreversible, false))
|
||||
form.WholeWord = util.Ptr(util.PtrOrValue(form.WholeWord, false))
|
||||
form.Irreversible = util.Ptr(util.PtrOrValue(form.Irreversible, false))
|
||||
|
||||
if *form.Irreversible {
|
||||
return errors.New("irreversible aka server-side drop filters are not supported yet")
|
||||
|
|
|
@ -100,7 +100,7 @@ func (suite *FiltersTestSuite) TestGetFilterKeyword() {
|
|||
suite.NotEmpty(filterKeyword)
|
||||
suite.Equal(expectedFilterKeyword.ID, filterKeyword.ID)
|
||||
suite.Equal(expectedFilterKeyword.Keyword, filterKeyword.Keyword)
|
||||
suite.Equal(util.PtrValueOr(expectedFilterKeyword.WholeWord, false), filterKeyword.WholeWord)
|
||||
suite.Equal(util.PtrOrValue(expectedFilterKeyword.WholeWord, false), filterKeyword.WholeWord)
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestGetAnotherAccountsFilterKeyword() {
|
||||
|
|
|
@ -147,7 +147,7 @@ func validateNormalizeCreateUpdateFilterKeyword(form *apimodel.FilterKeywordCrea
|
|||
return err
|
||||
}
|
||||
|
||||
form.WholeWord = util.Ptr(util.PtrValueOr(form.WholeWord, false))
|
||||
form.WholeWord = util.Ptr(util.PtrOrValue(form.WholeWord, false))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -192,7 +192,7 @@ func validateNormalizeCreateFilter(form *apimodel.FilterCreateRequestV2) error {
|
|||
if err := validate.FilterTitle(form.Title); err != nil {
|
||||
return err
|
||||
}
|
||||
action := util.PtrValueOr(form.FilterAction, apimodel.FilterActionWarn)
|
||||
action := util.PtrOrValue(form.FilterAction, apimodel.FilterActionWarn)
|
||||
if err := validate.FilterAction(action); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -253,7 +253,7 @@ func validateNormalizeCreateFilter(form *apimodel.FilterCreateRequestV2) error {
|
|||
if err := validate.FilterKeyword(formKeyword.Keyword); err != nil {
|
||||
return err
|
||||
}
|
||||
form.Keywords[i].WholeWord = util.Ptr(util.PtrValueOr(formKeyword.WholeWord, false))
|
||||
form.Keywords[i].WholeWord = util.Ptr(util.PtrOrValue(formKeyword.WholeWord, false))
|
||||
}
|
||||
for _, formStatus := range form.Statuses {
|
||||
if err := validate.ULID(formStatus.StatusID, "status_id"); err != nil {
|
||||
|
|
|
@ -289,7 +289,7 @@ func validateNormalizeUpdateFilter(form *apimodel.FilterUpdateRequestV2) error {
|
|||
}
|
||||
}
|
||||
|
||||
destroy := util.PtrValueOr(formKeyword.Destroy, false)
|
||||
destroy := util.PtrOrValue(formKeyword.Destroy, false)
|
||||
form.Keywords[i].Destroy = &destroy
|
||||
|
||||
if destroy && formKeyword.ID == nil {
|
||||
|
@ -305,7 +305,7 @@ func validateNormalizeUpdateFilter(form *apimodel.FilterUpdateRequestV2) error {
|
|||
}
|
||||
}
|
||||
|
||||
destroy := util.PtrValueOr(formStatus.Destroy, false)
|
||||
destroy := util.PtrOrValue(formStatus.Destroy, false)
|
||||
form.Statuses[i].Destroy = &destroy
|
||||
|
||||
switch {
|
||||
|
|
|
@ -78,12 +78,12 @@ func init() {
|
|||
CreatedAt: account.CreatedAt,
|
||||
Reason: account.Reason,
|
||||
Privacy: newgtsmodel.Visibility(account.Privacy),
|
||||
Sensitive: util.Ptr(util.PtrValueOr(account.Sensitive, false)),
|
||||
Sensitive: util.Ptr(util.PtrOrValue(account.Sensitive, false)),
|
||||
Language: account.Language,
|
||||
StatusContentType: account.StatusContentType,
|
||||
CustomCSS: account.CustomCSS,
|
||||
EnableRSS: util.Ptr(util.PtrValueOr(account.EnableRSS, false)),
|
||||
HideCollections: util.Ptr(util.PtrValueOr(account.HideCollections, false)),
|
||||
EnableRSS: util.Ptr(util.PtrOrValue(account.EnableRSS, false)),
|
||||
HideCollections: util.Ptr(util.PtrOrValue(account.HideCollections, false)),
|
||||
}
|
||||
|
||||
// Insert the settings model.
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
// 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"
|
||||
|
||||
old_gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20240715204203_media_pipeline_improvements"
|
||||
new_gtsmodel "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
func init() {
|
||||
up := func(ctx context.Context, db *bun.DB) error {
|
||||
if err := db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
if _, err := tx.NewAddColumn().
|
||||
Table("media_attachments").
|
||||
ColumnExpr("? INTEGER NOT NULL DEFAULT ?", bun.Ident("type_new"), 0).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for old, new := range map[old_gtsmodel.FileType]new_gtsmodel.FileType{
|
||||
old_gtsmodel.FileTypeAudio: new_gtsmodel.FileTypeAudio,
|
||||
old_gtsmodel.FileTypeImage: new_gtsmodel.FileTypeImage,
|
||||
old_gtsmodel.FileTypeGifv: new_gtsmodel.FileTypeImage,
|
||||
old_gtsmodel.FileTypeVideo: new_gtsmodel.FileTypeVideo,
|
||||
old_gtsmodel.FileTypeUnknown: new_gtsmodel.FileTypeUnknown,
|
||||
} {
|
||||
if _, err := tx.NewUpdate().
|
||||
Table("media_attachments").
|
||||
Where("? = ?", bun.Ident("type"), old).
|
||||
Set("? = ?", bun.Ident("type_new"), new).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := tx.NewDropColumn().
|
||||
Table("media_attachments").
|
||||
ColumnExpr("?", bun.Ident("type")).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.NewRaw(
|
||||
"ALTER TABLE ? RENAME COLUMN ? TO ?",
|
||||
bun.Ident("media_attachments"),
|
||||
bun.Ident("type_new"),
|
||||
bun.Ident("type"),
|
||||
).Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Zero-out attachment data
|
||||
// for "unknown" non-locally
|
||||
// stored media attachments.
|
||||
if _, err := db.NewUpdate().
|
||||
Table("media_attachments").
|
||||
Where("? = ?", bun.Ident("type"), new_gtsmodel.FileTypeUnknown).
|
||||
Set("? = ?", bun.Ident("url"), "").
|
||||
Set("? = ?", bun.Ident("file_path"), "").
|
||||
Set("? = ?", bun.Ident("file_content_type"), "").
|
||||
Set("? = ?", bun.Ident("file_file_size"), 0).
|
||||
Set("? = ?", bun.Ident("thumbnail_path"), "").
|
||||
Set("? = ?", bun.Ident("thumbnail_content_type"), "").
|
||||
Set("? = ?", bun.Ident("thumbnail_file_size"), 0).
|
||||
Set("? = ?", bun.Ident("thumbnail_url"), "").
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Zero-out emoji data for
|
||||
// non-locally stored emoji.
|
||||
if _, err := db.NewUpdate().
|
||||
Table("emojis").
|
||||
WhereOr("? = ?", bun.Ident("image_url"), "").
|
||||
WhereOr("? = ?", bun.Ident("image_path"), "").
|
||||
Set("? = ?", bun.Ident("image_path"), "").
|
||||
Set("? = ?", bun.Ident("image_url"), "").
|
||||
Set("? = ?", bun.Ident("image_file_size"), 0).
|
||||
Set("? = ?", bun.Ident("image_content_type"), "").
|
||||
Set("? = ?", bun.Ident("image_static_path"), "").
|
||||
Set("? = ?", bun.Ident("image_static_url"), "").
|
||||
Set("? = ?", bun.Ident("image_static_file_size"), 0).
|
||||
Set("? = ?", bun.Ident("image_static_content_type"), "").
|
||||
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,65 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package gtsmodel
|
||||
|
||||
import "time"
|
||||
|
||||
// Emoji represents a custom emoji that's been uploaded through the admin UI or downloaded from a remote instance.
|
||||
type Emoji struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||
Shortcode string `bun:",nullzero,notnull,unique:domainshortcode"` // String shortcode for this emoji -- the part that's between colons. This should be a-zA-Z_ eg., 'blob_hug' 'purple_heart' 'Gay_Otter' Must be unique with domain.
|
||||
Domain string `bun:",nullzero,unique:domainshortcode"` // Origin domain of this emoji, eg 'example.org', 'queer.party'. empty string for local emojis.
|
||||
ImageRemoteURL string `bun:",nullzero"` // Where can this emoji be retrieved remotely? Null for local emojis.
|
||||
ImageStaticRemoteURL string `bun:",nullzero"` // Where can a static / non-animated version of this emoji be retrieved remotely? Null for local emojis.
|
||||
ImageURL string `bun:",nullzero"` // Where can this emoji be retrieved from the local server? Null for remote emojis.
|
||||
ImageStaticURL string `bun:",nullzero"` // Where can a static version of this emoji be retrieved from the local server? Null for remote emojis.
|
||||
ImagePath string `bun:",notnull"` // Path of the emoji image in the server storage system.
|
||||
ImageStaticPath string `bun:",notnull"` // Path of a static version of the emoji image in the server storage system
|
||||
ImageContentType string `bun:",notnull"` // MIME content type of the emoji image
|
||||
ImageStaticContentType string `bun:",notnull"` // MIME content type of the static version of the emoji image.
|
||||
ImageFileSize int `bun:",notnull"` // Size of the emoji image file in bytes, for serving purposes.
|
||||
ImageStaticFileSize int `bun:",notnull"` // Size of the static version of the emoji image file in bytes, for serving purposes.
|
||||
Disabled *bool `bun:",nullzero,notnull,default:false"` // Has a moderation action disabled this emoji from being shown?
|
||||
URI string `bun:",nullzero,notnull,unique"` // ActivityPub uri of this emoji. Something like 'https://example.org/emojis/1234'
|
||||
VisibleInPicker *bool `bun:",nullzero,notnull,default:true"` // Is this emoji visible in the admin emoji picker?
|
||||
Category *EmojiCategory `bun:"rel:belongs-to"` // In which emoji category is this emoji visible?
|
||||
CategoryID string `bun:"type:CHAR(26),nullzero"` // ID of the category this emoji belongs to.
|
||||
Cached *bool `bun:",nullzero,notnull,default:false"` // whether emoji is cached in locally in gotosocial storage.
|
||||
}
|
||||
|
||||
// IsLocal returns true if the emoji is
|
||||
// local to this instance., ie., it did
|
||||
// not originate from a remote instance.
|
||||
func (e *Emoji) IsLocal() bool {
|
||||
return e.Domain == ""
|
||||
}
|
||||
|
||||
// ShortcodeDomain returns the [shortcode]@[domain] for the given emoji.
|
||||
func (e *Emoji) ShortcodeDomain() string {
|
||||
return e.Shortcode + "@" + e.Domain
|
||||
}
|
||||
|
||||
// EmojiCategory represents a grouping of custom emojis.
|
||||
type EmojiCategory struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||
Name string `bun:",nullzero,notnull,unique"` // name of this category
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package gtsmodel
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// MediaAttachment represents a user-uploaded media attachment: an image/video/audio/gif that is
|
||||
// somewhere in storage and that can be retrieved and served by the router.
|
||||
type MediaAttachment struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||
StatusID string `bun:"type:CHAR(26),nullzero"` // ID of the status to which this is attached
|
||||
URL string `bun:",nullzero"` // Where can the attachment be retrieved on *this* server
|
||||
RemoteURL string `bun:",nullzero"` // Where can the attachment be retrieved on a remote server (empty for local media)
|
||||
Type FileType `bun:",notnull"` // Type of file (image/gifv/audio/video/unknown)
|
||||
FileMeta FileMeta `bun:",embed:,notnull"` // Metadata about the file
|
||||
AccountID string `bun:"type:CHAR(26),nullzero,notnull"` // To which account does this attachment belong
|
||||
Description string `bun:""` // Description of the attachment (for screenreaders)
|
||||
ScheduledStatusID string `bun:"type:CHAR(26),nullzero"` // To which scheduled status does this attachment belong
|
||||
Blurhash string `bun:",nullzero"` // What is the generated blurhash of this attachment
|
||||
Processing ProcessingStatus `bun:",notnull,default:2"` // What is the processing status of this attachment
|
||||
File File `bun:",embed:file_,notnull,nullzero"` // metadata for the whole file
|
||||
Thumbnail Thumbnail `bun:",embed:thumbnail_,notnull,nullzero"` // small image thumbnail derived from a larger image, video, or audio file.
|
||||
Avatar *bool `bun:",nullzero,notnull,default:false"` // Is this attachment being used as an avatar?
|
||||
Header *bool `bun:",nullzero,notnull,default:false"` // Is this attachment being used as a header?
|
||||
Cached *bool `bun:",nullzero,notnull,default:false"` // Is this attachment currently cached by our instance?
|
||||
}
|
||||
|
||||
// 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
|
||||
type File struct {
|
||||
Path string `bun:",notnull"` // Path of the file in storage.
|
||||
ContentType string `bun:",notnull"` // MIME content type of the file.
|
||||
FileSize int `bun:",notnull"` // File size in bytes
|
||||
}
|
||||
|
||||
// Thumbnail refers to a small image thumbnail derived from a larger image, video, or audio file.
|
||||
type Thumbnail struct {
|
||||
Path string `bun:",notnull"` // Path of the file in storage.
|
||||
ContentType string `bun:",notnull"` // MIME content type of the file.
|
||||
FileSize int `bun:",notnull"` // File size in bytes
|
||||
URL string `bun:",nullzero"` // What is the URL of the thumbnail on the local server
|
||||
RemoteURL string `bun:",nullzero"` // What is the remote URL of the thumbnail (empty for local media)
|
||||
}
|
||||
|
||||
// ProcessingStatus refers to how far along in the processing stage the attachment is.
|
||||
type ProcessingStatus int
|
||||
|
||||
// MediaAttachment processing states.
|
||||
const (
|
||||
ProcessingStatusReceived ProcessingStatus = 0 // ProcessingStatusReceived indicates the attachment has been received and is awaiting processing. No thumbnail available yet.
|
||||
ProcessingStatusProcessing ProcessingStatus = 1 // ProcessingStatusProcessing indicates the attachment is currently being processed. Thumbnail is available but full media is not.
|
||||
ProcessingStatusProcessed ProcessingStatus = 2 // ProcessingStatusProcessed indicates the attachment has been fully processed and is ready to be served.
|
||||
ProcessingStatusError ProcessingStatus = 666 // ProcessingStatusError indicates something went wrong processing the attachment and it won't be tried again--these can be deleted.
|
||||
)
|
||||
|
||||
// FileType refers to the file type of the media attaachment.
|
||||
type FileType string
|
||||
|
||||
// MediaAttachment file types.
|
||||
const (
|
||||
FileTypeImage FileType = "Image" // FileTypeImage is for jpegs, pngs, and standard gifs
|
||||
FileTypeGifv FileType = "Gifv" // FileTypeGif is for soundless looping videos that behave like gifs
|
||||
FileTypeAudio FileType = "Audio" // FileTypeAudio is for audio-only files (no video)
|
||||
FileTypeVideo FileType = "Video" // FileTypeVideo is for files with audio + visual
|
||||
FileTypeUnknown FileType = "Unknown" // FileTypeUnknown is for unknown file types (surprise surprise!)
|
||||
)
|
||||
|
||||
// FileMeta describes metadata about the actual contents of the file.
|
||||
type FileMeta struct {
|
||||
Original Original `bun:"embed:original_"`
|
||||
Small Small `bun:"embed:small_"`
|
||||
Focus Focus `bun:"embed:focus_"`
|
||||
}
|
||||
|
||||
// Small can be used for a thumbnail of any media type
|
||||
type Small struct {
|
||||
Width int // width in pixels
|
||||
Height int // height in pixels
|
||||
Size int // size in pixels (width * height)
|
||||
Aspect float32 // aspect ratio (width / height)
|
||||
}
|
||||
|
||||
// Original can be used for original metadata for any media type
|
||||
type Original struct {
|
||||
Width int // width in pixels
|
||||
Height int // height in pixels
|
||||
Size int // size in pixels (width * height)
|
||||
Aspect float32 // aspect ratio (width / height)
|
||||
Duration *float32 // video-specific: duration of the video in seconds
|
||||
Framerate *float32 // video-specific: fps
|
||||
Bitrate *uint64 // video-specific: bitrate
|
||||
}
|
||||
|
||||
// Focus describes the 'center' of the image for display purposes.
|
||||
// X and Y should each be between -1 and 1
|
||||
type Focus struct {
|
||||
X float32
|
||||
Y float32
|
||||
}
|
|
@ -30,7 +30,7 @@ type MediaAttachment struct {
|
|||
StatusID string `bun:"type:CHAR(26),nullzero"` // ID of the status to which this is attached
|
||||
URL string `bun:",nullzero"` // Where can the attachment be retrieved on *this* server
|
||||
RemoteURL string `bun:",nullzero"` // Where can the attachment be retrieved on a remote server (empty for local media)
|
||||
Type FileType `bun:",notnull"` // Type of file (image/gifv/audio/video/unknown)
|
||||
Type FileType `bun:",notnull,default:0"` // Type of file (image/gifv/audio/video/unknown)
|
||||
FileMeta FileMeta `bun:",embed:,notnull"` // Metadata about the file
|
||||
AccountID string `bun:"type:CHAR(26),nullzero,notnull"` // To which account does this attachment belong
|
||||
Description string `bun:""` // Description of the attachment (for screenreaders)
|
||||
|
@ -81,18 +81,34 @@ const (
|
|||
ProcessingStatusError ProcessingStatus = 666 // ProcessingStatusError indicates something went wrong processing the attachment and it won't be tried again--these can be deleted.
|
||||
)
|
||||
|
||||
// FileType refers to the file type of the media attaachment.
|
||||
type FileType string
|
||||
// FileType refers to the file
|
||||
// type of the media attaachment.
|
||||
type FileType int
|
||||
|
||||
// MediaAttachment file types.
|
||||
const (
|
||||
FileTypeImage FileType = "Image" // FileTypeImage is for jpegs, pngs, and standard gifs
|
||||
FileTypeGifv FileType = "Gifv" // FileTypeGif is for soundless looping videos that behave like gifs
|
||||
FileTypeAudio FileType = "Audio" // FileTypeAudio is for audio-only files (no video)
|
||||
FileTypeVideo FileType = "Video" // FileTypeVideo is for files with audio + visual
|
||||
FileTypeUnknown FileType = "Unknown" // FileTypeUnknown is for unknown file types (surprise surprise!)
|
||||
// MediaAttachment file types.
|
||||
FileTypeUnknown FileType = 0 // FileTypeUnknown is for unknown file types (surprise surprise!)
|
||||
FileTypeImage FileType = 1 // FileTypeImage is for jpegs, pngs, and standard gifs
|
||||
FileTypeAudio FileType = 2 // FileTypeAudio is for audio-only files (no video)
|
||||
FileTypeVideo FileType = 3 // FileTypeVideo is for files with audio + visual
|
||||
)
|
||||
|
||||
// String returns a stringified, frontend API compatible form of FileType.
|
||||
func (t FileType) String() string {
|
||||
switch t {
|
||||
case FileTypeUnknown:
|
||||
return "unknown"
|
||||
case FileTypeImage:
|
||||
return "image"
|
||||
case FileTypeAudio:
|
||||
return "audio"
|
||||
case FileTypeVideo:
|
||||
return "video"
|
||||
default:
|
||||
panic("invalid filetype")
|
||||
}
|
||||
}
|
||||
|
||||
// FileMeta describes metadata about the actual contents of the file.
|
||||
type FileMeta struct {
|
||||
Original Original `bun:"embed:original_"`
|
||||
|
|
|
@ -34,14 +34,33 @@ type wasmInstancePool struct {
|
|||
}
|
||||
|
||||
func (p *wasmInstancePool) Init(ctx context.Context, sz int) error {
|
||||
p.pool = make(chan *wasm.Instance, sz)
|
||||
for i := 0; i < sz; i++ {
|
||||
// Initialize for first time
|
||||
// to preload module into the
|
||||
// wazero compilation cache.
|
||||
inst, err := p.inst.New(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.pool <- inst
|
||||
|
||||
// Clamp to 1.
|
||||
if sz <= 0 {
|
||||
sz = 1
|
||||
}
|
||||
|
||||
// Allocate new pool instance channel.
|
||||
p.pool = make(chan *wasm.Instance, sz)
|
||||
|
||||
// Store only one
|
||||
// open instance
|
||||
// at init time.
|
||||
p.pool <- inst
|
||||
|
||||
// Fill reminaing with closed
|
||||
// instances for later opening.
|
||||
for i := 0; i < sz-1; i++ {
|
||||
p.pool <- new(wasm.Instance)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -102,74 +102,19 @@ func (m *Manager) CreateMedia(
|
|||
) {
|
||||
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 jpeg.
|
||||
"jpeg",
|
||||
)
|
||||
|
||||
// Calculate attachment thumbnail URL.
|
||||
thumbURL := uris.URIForAttachment(
|
||||
accountID,
|
||||
string(TypeAttachment),
|
||||
string(SizeSmall),
|
||||
id,
|
||||
|
||||
// Always encode attachment
|
||||
// thumbnails as jpeg.
|
||||
"jpeg",
|
||||
)
|
||||
|
||||
// Populate initial fields on the new media,
|
||||
// leaving out fields with values we don't know
|
||||
// yet. These will be overwritten as we go.
|
||||
attachment := >smodel.MediaAttachment{
|
||||
ID: id,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
URL: url,
|
||||
Type: gtsmodel.FileTypeUnknown,
|
||||
ID: id.NewULID(),
|
||||
AccountID: accountID,
|
||||
Type: gtsmodel.FileTypeUnknown,
|
||||
Processing: gtsmodel.ProcessingStatusReceived,
|
||||
File: gtsmodel.File{
|
||||
ContentType: "application/octet-stream",
|
||||
Path: path,
|
||||
},
|
||||
Thumbnail: gtsmodel.Thumbnail{
|
||||
ContentType: "image/jpeg",
|
||||
Path: thumbPath,
|
||||
URL: thumbURL,
|
||||
},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(false),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
// Check if we were provided additional info
|
||||
|
@ -252,42 +197,12 @@ func (m *Manager) CreateEmoji(
|
|||
// Generate new ID.
|
||||
id := id.NewULID()
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
if domain == "" && info.URI == nil {
|
||||
// Generate URI for local emoji.
|
||||
uri := uris.URIForEmoji(id)
|
||||
info.URI = &uri
|
||||
}
|
||||
|
||||
// Generate static URL for attachment.
|
||||
staticURL := uris.URIForAttachment(
|
||||
instanceAcc.ID,
|
||||
string(TypeEmoji),
|
||||
string(SizeStatic),
|
||||
id,
|
||||
|
||||
// All static emojis
|
||||
// are encoded as png.
|
||||
"png",
|
||||
)
|
||||
|
||||
// Generate static image path for attachment.
|
||||
staticPath := uris.StoragePathForAttachment(
|
||||
instanceAcc.ID,
|
||||
string(TypeEmoji),
|
||||
string(SizeStatic),
|
||||
id,
|
||||
|
||||
// All static emojis
|
||||
// are encoded as png.
|
||||
"png",
|
||||
)
|
||||
|
||||
// Populate initial fields on the new emoji,
|
||||
// leaving out fields with values we don't know
|
||||
// yet. These will be overwritten as we go.
|
||||
|
@ -295,9 +210,6 @@ func (m *Manager) CreateEmoji(
|
|||
ID: id,
|
||||
Shortcode: shortcode,
|
||||
Domain: domain,
|
||||
ImageStaticURL: staticURL,
|
||||
ImageStaticPath: staticPath,
|
||||
ImageStaticContentType: "image/png",
|
||||
Disabled: util.Ptr(false),
|
||||
VisibleInPicker: util.Ptr(true),
|
||||
CreatedAt: now,
|
||||
|
@ -327,12 +239,6 @@ func (m *Manager) RefreshEmoji(
|
|||
*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
|
||||
|
@ -380,38 +286,6 @@ func (m *Manager) RefreshEmoji(
|
|||
return rct, 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.
|
||||
"png",
|
||||
)
|
||||
|
||||
// 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.
|
||||
"png",
|
||||
)
|
||||
|
||||
// Finally, create new emoji in database.
|
||||
processingEmoji, err := m.createEmoji(ctx,
|
||||
func(ctx context.Context, emoji *gtsmodel.Emoji) error {
|
||||
|
@ -425,8 +299,8 @@ func (m *Manager) RefreshEmoji(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Set the refreshed path ID used.
|
||||
processingEmoji.newPathID = newPathID
|
||||
// Generate a new path ID to use instead.
|
||||
processingEmoji.newPathID = id.NewULID()
|
||||
|
||||
return processingEmoji, nil
|
||||
}
|
||||
|
@ -441,6 +315,12 @@ func (m *Manager) createEmoji(
|
|||
*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)
|
||||
}
|
||||
|
||||
// Check if we have additional info to add to the emoji,
|
||||
// and overwrite some of the emoji fields if so.
|
||||
if info.URI != nil {
|
||||
|
@ -475,6 +355,7 @@ func (m *Manager) createEmoji(
|
|||
|
||||
// Return wrapped emoji for later processing.
|
||||
processingEmoji := &ProcessingEmoji{
|
||||
instAccID: instanceAcc.ID,
|
||||
emoji: emoji,
|
||||
dataFn: data,
|
||||
mgr: m,
|
||||
|
|
|
@ -358,11 +358,10 @@ func (suite *ManagerTestSuite) TestPDFProcess() {
|
|||
suite.Equal(processing.ID(), attachment.ID)
|
||||
suite.Equal(accountID, attachment.AccountID)
|
||||
|
||||
// file meta should be correctly derived from the image
|
||||
suite.Zero(attachment.FileMeta)
|
||||
suite.Equal("application/octet-stream", attachment.File.ContentType)
|
||||
suite.Equal("image/jpeg", attachment.Thumbnail.ContentType)
|
||||
suite.Empty(attachment.Blurhash)
|
||||
suite.Zero(attachment.File.ContentType)
|
||||
suite.Zero(attachment.Thumbnail.ContentType)
|
||||
suite.Zero(attachment.Blurhash)
|
||||
|
||||
// now make sure the attachment is in the database
|
||||
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachment.ID)
|
||||
|
@ -376,7 +375,6 @@ func (suite *ManagerTestSuite) TestPDFProcess() {
|
|||
stored, err := suite.storage.Has(ctx, attachment.File.Path)
|
||||
suite.NoError(err)
|
||||
suite.False(stored)
|
||||
|
||||
stored, err = suite.storage.Has(ctx, attachment.Thumbnail.Path)
|
||||
suite.NoError(err)
|
||||
suite.False(stored)
|
||||
|
|
|
@ -26,7 +26,6 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/regexes"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/storage"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
|
@ -36,6 +35,7 @@ import (
|
|||
// various functions for retrieving data from the process.
|
||||
type ProcessingEmoji struct {
|
||||
emoji *gtsmodel.Emoji // processing emoji details
|
||||
instAccID string // instance account ID
|
||||
newPathID string // new emoji path ID to use when being refreshed
|
||||
dataFn DataFunc // load-data function, returns media stream
|
||||
done bool // done is set when process finishes with non ctx canceled type error
|
||||
|
@ -191,21 +191,24 @@ func (p *ProcessingEmoji) store(ctx context.Context) error {
|
|||
pathID = p.emoji.ID
|
||||
}
|
||||
|
||||
// Determine instance account ID from generated image static path.
|
||||
instanceAccID, ok := getInstanceAccountID(p.emoji.ImageStaticPath)
|
||||
if !ok {
|
||||
return gtserror.Newf("invalid emoji static path; no instance account id: %s", p.emoji.ImageStaticPath)
|
||||
}
|
||||
|
||||
// Calculate final media attachment file path.
|
||||
// Calculate final emoji media file path.
|
||||
p.emoji.ImagePath = uris.StoragePathForAttachment(
|
||||
instanceAccID,
|
||||
p.instAccID,
|
||||
string(TypeEmoji),
|
||||
string(SizeOriginal),
|
||||
pathID,
|
||||
ext,
|
||||
)
|
||||
|
||||
// Calculate final emoji static media file path.
|
||||
p.emoji.ImageStaticPath = uris.StoragePathForAttachment(
|
||||
p.instAccID,
|
||||
string(TypeEmoji),
|
||||
string(SizeStatic),
|
||||
pathID,
|
||||
"png",
|
||||
)
|
||||
|
||||
// Copy temporary file into storage at path.
|
||||
filesz, err := p.mgr.state.Storage.PutFile(ctx,
|
||||
p.emoji.ImagePath,
|
||||
|
@ -228,19 +231,31 @@ func (p *ProcessingEmoji) store(ctx context.Context) error {
|
|||
p.emoji.ImageFileSize = int(filesz)
|
||||
p.emoji.ImageStaticFileSize = int(staticsz)
|
||||
|
||||
// Fill in remaining emoji data now it's stored.
|
||||
// Generate an emoji media static URL.
|
||||
p.emoji.ImageURL = uris.URIForAttachment(
|
||||
instanceAccID,
|
||||
p.instAccID,
|
||||
string(TypeEmoji),
|
||||
string(SizeOriginal),
|
||||
pathID,
|
||||
ext,
|
||||
)
|
||||
|
||||
// Generate an emoji image static URL.
|
||||
p.emoji.ImageStaticURL = uris.URIForAttachment(
|
||||
p.instAccID,
|
||||
string(TypeEmoji),
|
||||
string(SizeStatic),
|
||||
pathID,
|
||||
"png",
|
||||
)
|
||||
|
||||
// Get mimetype for the file container
|
||||
// type, falling back to generic data.
|
||||
p.emoji.ImageContentType = getMimeType(ext)
|
||||
|
||||
// Set the known emoji static content type.
|
||||
p.emoji.ImageStaticContentType = "image/png"
|
||||
|
||||
// We can now consider this cached.
|
||||
p.emoji.Cached = util.Ptr(true)
|
||||
|
||||
|
@ -268,16 +283,16 @@ func (p *ProcessingEmoji) cleanup(ctx context.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
// Unset processor-calculated fields.
|
||||
p.emoji.ImageStaticContentType = ""
|
||||
p.emoji.ImageStaticFileSize = 0
|
||||
p.emoji.ImageStaticPath = ""
|
||||
p.emoji.ImageStaticURL = ""
|
||||
p.emoji.ImageContentType = ""
|
||||
p.emoji.ImageFileSize = 0
|
||||
p.emoji.ImagePath = ""
|
||||
p.emoji.ImageURL = ""
|
||||
|
||||
// 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
|
||||
}
|
||||
|
|
|
@ -248,6 +248,15 @@ func (p *ProcessingMedia) store(ctx context.Context) error {
|
|||
return gtserror.Newf("error generating thumb blurhash: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate final media attachment thumbnail path.
|
||||
p.media.Thumbnail.Path = uris.StoragePathForAttachment(
|
||||
p.media.AccountID,
|
||||
string(TypeAttachment),
|
||||
string(SizeSmall),
|
||||
p.media.ID,
|
||||
"jpeg",
|
||||
)
|
||||
}
|
||||
|
||||
// Calculate final media attachment file path.
|
||||
|
@ -285,8 +294,7 @@ func (p *ProcessingMedia) store(ctx context.Context) error {
|
|||
p.media.Thumbnail.FileSize = int(thumbsz)
|
||||
}
|
||||
|
||||
// Fill in correct attachment
|
||||
// data now we've parsed it.
|
||||
// Generate a media attachment URL.
|
||||
p.media.URL = uris.URIForAttachment(
|
||||
p.media.AccountID,
|
||||
string(TypeAttachment),
|
||||
|
@ -295,10 +303,22 @@ func (p *ProcessingMedia) store(ctx context.Context) error {
|
|||
ext,
|
||||
)
|
||||
|
||||
// Generate a media attachment thumbnail URL.
|
||||
p.media.Thumbnail.URL = uris.URIForAttachment(
|
||||
p.media.AccountID,
|
||||
string(TypeAttachment),
|
||||
string(SizeSmall),
|
||||
p.media.ID,
|
||||
"jpeg",
|
||||
)
|
||||
|
||||
// Get mimetype for the file container
|
||||
// type, falling back to generic data.
|
||||
p.media.File.ContentType = getMimeType(ext)
|
||||
|
||||
// Set the known thumbnail content type.
|
||||
p.media.Thumbnail.ContentType = "image/jpeg"
|
||||
|
||||
// We can now consider this cached.
|
||||
p.media.Cached = util.Ptr(true)
|
||||
|
||||
|
@ -329,6 +349,18 @@ func (p *ProcessingMedia) cleanup(ctx context.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
// Unset all processor-calculated media fields.
|
||||
p.media.FileMeta.Original = gtsmodel.Original{}
|
||||
p.media.FileMeta.Small = gtsmodel.Small{}
|
||||
p.media.File.ContentType = ""
|
||||
p.media.File.FileSize = 0
|
||||
p.media.File.Path = ""
|
||||
p.media.Thumbnail.FileSize = 0
|
||||
p.media.Thumbnail.ContentType = ""
|
||||
p.media.Thumbnail.Path = ""
|
||||
p.media.Thumbnail.URL = ""
|
||||
p.media.URL = ""
|
||||
|
||||
// Also ensure marked as unknown and finished
|
||||
// processing so gets inserted as placeholder URL.
|
||||
p.media.Processing = gtsmodel.ProcessingStatusProcessed
|
||||
|
|
|
@ -117,8 +117,8 @@ func (p *Processor) FollowCreate(ctx context.Context, requestingAccount *gtsmode
|
|||
if targetAccount.IsLocal() && !*targetAccount.Locked {
|
||||
rel.Requested = false
|
||||
rel.Following = true
|
||||
rel.ShowingReblogs = util.PtrValueOr(fr.ShowReblogs, true)
|
||||
rel.Notifying = util.PtrValueOr(fr.Notify, false)
|
||||
rel.ShowingReblogs = util.PtrOrValue(fr.ShowReblogs, true)
|
||||
rel.Notifying = util.PtrOrValue(fr.Notify, false)
|
||||
}
|
||||
|
||||
// Handle side effects async.
|
||||
|
|
|
@ -325,8 +325,8 @@ func (p *Processor) emojiUpdateCopy(
|
|||
|
||||
// Attempt to create the new local emoji.
|
||||
emoji, errWithCode := p.createEmoji(ctx,
|
||||
util.PtrValueOr(shortcode, ""),
|
||||
util.PtrValueOr(categoryName, ""),
|
||||
util.PtrOrValue(shortcode, ""),
|
||||
util.PtrOrValue(categoryName, ""),
|
||||
data,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
|
|
|
@ -71,7 +71,7 @@ func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form
|
|||
FilterID: filter.ID,
|
||||
Filter: filter,
|
||||
Keyword: form.Phrase,
|
||||
WholeWord: util.Ptr(util.PtrValueOr(form.WholeWord, false)),
|
||||
WholeWord: util.Ptr(util.PtrOrValue(form.WholeWord, false)),
|
||||
}
|
||||
filter.Keywords = []*gtsmodel.FilterKeyword{filterKeyword}
|
||||
|
||||
|
|
|
@ -108,11 +108,11 @@ func (p *Processor) Update(
|
|||
if expiresAt != filter.ExpiresAt {
|
||||
forbiddenFields = append(forbiddenFields, "expires_in")
|
||||
}
|
||||
if contextHome != util.PtrValueOr(filter.ContextHome, false) ||
|
||||
contextNotifications != util.PtrValueOr(filter.ContextNotifications, false) ||
|
||||
contextPublic != util.PtrValueOr(filter.ContextPublic, false) ||
|
||||
contextThread != util.PtrValueOr(filter.ContextThread, false) ||
|
||||
contextAccount != util.PtrValueOr(filter.ContextAccount, false) {
|
||||
if contextHome != util.PtrOrValue(filter.ContextHome, false) ||
|
||||
contextNotifications != util.PtrOrValue(filter.ContextNotifications, false) ||
|
||||
contextPublic != util.PtrOrValue(filter.ContextPublic, false) ||
|
||||
contextThread != util.PtrOrValue(filter.ContextThread, false) ||
|
||||
contextAccount != util.PtrOrValue(filter.ContextAccount, false) {
|
||||
forbiddenFields = append(forbiddenFields, "context")
|
||||
}
|
||||
if len(forbiddenFields) > 0 {
|
||||
|
@ -132,7 +132,7 @@ func (p *Processor) Update(
|
|||
filter.ContextThread = &contextThread
|
||||
filter.ContextAccount = &contextAccount
|
||||
filterKeyword.Keyword = form.Phrase
|
||||
filterKeyword.WholeWord = util.Ptr(util.PtrValueOr(form.WholeWord, false))
|
||||
filterKeyword.WholeWord = util.Ptr(util.PtrOrValue(form.WholeWord, false))
|
||||
|
||||
// We only want to update the relevant filter keyword.
|
||||
filter.Keywords = []*gtsmodel.FilterKeyword{filterKeyword}
|
||||
|
|
|
@ -189,7 +189,7 @@ func applyKeywordChanges(filter *gtsmodel.Filter, formKeywords []apimodel.Filter
|
|||
FilterID: filter.ID,
|
||||
Filter: filter,
|
||||
Keyword: *formKeyword.Keyword,
|
||||
WholeWord: util.Ptr(util.PtrValueOr(formKeyword.WholeWord, false)),
|
||||
WholeWord: util.Ptr(util.PtrOrValue(formKeyword.WholeWord, false)),
|
||||
}
|
||||
filterKeywordsByID[filterKeyword.ID] = filterKeyword
|
||||
// Don't need to set columns, as we're using all of them.
|
||||
|
|
|
@ -26,7 +26,6 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"codeberg.org/gruf/go-bytesize"
|
||||
|
@ -245,13 +244,9 @@ func NewFileStorage() (*Driver, error) {
|
|||
// Load runtime configuration
|
||||
basePath := config.GetStorageLocalBasePath()
|
||||
|
||||
// Use default disk config but with
|
||||
// increased write buffer size and
|
||||
// 'exclusive' bit sets when creating
|
||||
// files to ensure we don't overwrite
|
||||
// existing files unless intending to.
|
||||
// Use default disk config with
|
||||
// increased write buffer size.
|
||||
diskCfg := disk.DefaultConfig()
|
||||
diskCfg.OpenWrite.Flags |= syscall.O_EXCL
|
||||
diskCfg.WriteBufSize = int(16 * bytesize.KiB)
|
||||
|
||||
// Open the disk storage implementation
|
||||
|
|
|
@ -21,8 +21,6 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -321,9 +319,9 @@ func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
|
|||
}
|
||||
|
||||
var (
|
||||
locked = util.PtrValueOr(a.Locked, true)
|
||||
discoverable = util.PtrValueOr(a.Discoverable, false)
|
||||
bot = util.PtrValueOr(a.Bot, false)
|
||||
locked = util.PtrOrValue(a.Locked, true)
|
||||
discoverable = util.PtrOrValue(a.Discoverable, false)
|
||||
bot = util.PtrOrValue(a.Bot, false)
|
||||
)
|
||||
|
||||
// Remaining properties are simple and
|
||||
|
@ -565,84 +563,59 @@ func (c *Converter) AppToAPIAppPublic(ctx context.Context, a *gtsmodel.Applicati
|
|||
}
|
||||
|
||||
// AttachmentToAPIAttachment converts a gts model media attacahment into its api representation for serialization on the API.
|
||||
func (c *Converter) AttachmentToAPIAttachment(ctx context.Context, a *gtsmodel.MediaAttachment) (apimodel.Attachment, error) {
|
||||
apiAttachment := apimodel.Attachment{
|
||||
ID: a.ID,
|
||||
Type: strings.ToLower(string(a.Type)),
|
||||
func (c *Converter) AttachmentToAPIAttachment(ctx context.Context, media *gtsmodel.MediaAttachment) (apimodel.Attachment, error) {
|
||||
var api apimodel.Attachment
|
||||
api.Type = media.Type.String()
|
||||
api.ID = media.ID
|
||||
|
||||
// Only add file details if
|
||||
// we have stored locally.
|
||||
if media.File.Path != "" {
|
||||
api.Meta = new(apimodel.MediaMeta)
|
||||
api.Meta.Original = apimodel.MediaDimensions{
|
||||
Width: media.FileMeta.Original.Width,
|
||||
Height: media.FileMeta.Original.Height,
|
||||
Aspect: media.FileMeta.Original.Aspect,
|
||||
Size: toAPISize(media.FileMeta.Original.Width, media.FileMeta.Original.Height),
|
||||
FrameRate: toAPIFrameRate(media.FileMeta.Original.Framerate),
|
||||
Duration: util.PtrOrZero(media.FileMeta.Original.Duration),
|
||||
Bitrate: int(util.PtrOrZero(media.FileMeta.Original.Bitrate)),
|
||||
}
|
||||
|
||||
// Don't try to serialize meta for
|
||||
// unknown attachments, there's no point.
|
||||
if a.Type != gtsmodel.FileTypeUnknown {
|
||||
apiAttachment.Meta = &apimodel.MediaMeta{
|
||||
Original: apimodel.MediaDimensions{
|
||||
Width: a.FileMeta.Original.Width,
|
||||
Height: a.FileMeta.Original.Height,
|
||||
},
|
||||
Small: apimodel.MediaDimensions{
|
||||
Width: a.FileMeta.Small.Width,
|
||||
Height: a.FileMeta.Small.Height,
|
||||
Size: strconv.Itoa(a.FileMeta.Small.Width) + "x" + strconv.Itoa(a.FileMeta.Small.Height),
|
||||
Aspect: float32(a.FileMeta.Small.Aspect),
|
||||
},
|
||||
// Copy over local file URL.
|
||||
api.URL = util.Ptr(media.URL)
|
||||
api.TextURL = util.Ptr(media.URL)
|
||||
|
||||
// Set file focus details.
|
||||
// (this doesn't make much sense if media
|
||||
// has no image, but the API doesn't yet
|
||||
// distinguish between zero values vs. none).
|
||||
api.Meta.Focus = new(apimodel.MediaFocus)
|
||||
api.Meta.Focus.X = media.FileMeta.Focus.X
|
||||
api.Meta.Focus.Y = media.FileMeta.Focus.Y
|
||||
|
||||
// Only add thumbnail details if
|
||||
// we have thumbnail stored locally.
|
||||
if media.Thumbnail.Path != "" {
|
||||
api.Meta.Small = apimodel.MediaDimensions{
|
||||
Width: media.FileMeta.Small.Width,
|
||||
Height: media.FileMeta.Small.Height,
|
||||
Aspect: media.FileMeta.Small.Aspect,
|
||||
Size: toAPISize(media.FileMeta.Small.Width, media.FileMeta.Small.Height),
|
||||
}
|
||||
|
||||
// Copy over local thumbnail file URL.
|
||||
api.PreviewURL = util.Ptr(media.Thumbnail.URL)
|
||||
}
|
||||
}
|
||||
|
||||
if i := a.Blurhash; i != "" {
|
||||
apiAttachment.Blurhash = &i
|
||||
}
|
||||
// Set remaining API attachment fields.
|
||||
api.Blurhash = util.PtrIf(media.Blurhash)
|
||||
api.RemoteURL = util.PtrIf(media.RemoteURL)
|
||||
api.PreviewRemoteURL = util.PtrIf(media.Thumbnail.RemoteURL)
|
||||
api.Description = util.PtrIf(media.Description)
|
||||
|
||||
if i := a.URL; i != "" {
|
||||
apiAttachment.URL = &i
|
||||
apiAttachment.TextURL = &i
|
||||
}
|
||||
|
||||
if i := a.Thumbnail.URL; i != "" {
|
||||
apiAttachment.PreviewURL = &i
|
||||
}
|
||||
|
||||
if i := a.RemoteURL; i != "" {
|
||||
apiAttachment.RemoteURL = &i
|
||||
}
|
||||
|
||||
if i := a.Thumbnail.RemoteURL; i != "" {
|
||||
apiAttachment.PreviewRemoteURL = &i
|
||||
}
|
||||
|
||||
if i := a.Description; i != "" {
|
||||
apiAttachment.Description = &i
|
||||
}
|
||||
|
||||
// Type-specific fields.
|
||||
switch a.Type {
|
||||
|
||||
case gtsmodel.FileTypeImage:
|
||||
apiAttachment.Meta.Original.Size = strconv.Itoa(a.FileMeta.Original.Width) + "x" + strconv.Itoa(a.FileMeta.Original.Height)
|
||||
apiAttachment.Meta.Original.Aspect = float32(a.FileMeta.Original.Aspect)
|
||||
apiAttachment.Meta.Focus = &apimodel.MediaFocus{
|
||||
X: a.FileMeta.Focus.X,
|
||||
Y: a.FileMeta.Focus.Y,
|
||||
}
|
||||
|
||||
case gtsmodel.FileTypeVideo, gtsmodel.FileTypeAudio:
|
||||
if i := a.FileMeta.Original.Duration; i != nil {
|
||||
apiAttachment.Meta.Original.Duration = *i
|
||||
}
|
||||
|
||||
if i := a.FileMeta.Original.Framerate; i != nil {
|
||||
// The masto api expects this as a string in
|
||||
// the format `integer/1`, so 30fps is `30/1`.
|
||||
round := math.Round(float64(*i))
|
||||
fr := strconv.Itoa(int(round))
|
||||
apiAttachment.Meta.Original.FrameRate = fr + "/1"
|
||||
}
|
||||
|
||||
if i := a.FileMeta.Original.Bitrate; i != nil {
|
||||
apiAttachment.Meta.Original.Bitrate = int(*i)
|
||||
}
|
||||
}
|
||||
|
||||
return apiAttachment, nil
|
||||
return api, nil
|
||||
}
|
||||
|
||||
// MentionToAPIMention converts a gts model mention into its api (frontend) representation for serialization on the API.
|
||||
|
@ -681,6 +654,7 @@ func (c *Converter) MentionToAPIMention(ctx context.Context, m *gtsmodel.Mention
|
|||
// EmojiToAPIEmoji converts a gts model emoji into its api (frontend) representation for serialization on the API.
|
||||
func (c *Converter) EmojiToAPIEmoji(ctx context.Context, e *gtsmodel.Emoji) (apimodel.Emoji, error) {
|
||||
var category string
|
||||
|
||||
if e.CategoryID != "" {
|
||||
if e.Category == nil {
|
||||
var err error
|
||||
|
@ -778,14 +752,15 @@ func (c *Converter) StatusToAPIStatus(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Normalize status for the API by pruning
|
||||
// out unknown attachment types and replacing
|
||||
// them with a helpful message.
|
||||
// Normalize status for API by pruning
|
||||
// attachments that were not locally
|
||||
// stored, replacing them with a helpful
|
||||
// message + links to remote.
|
||||
var aside string
|
||||
aside, apiStatus.MediaAttachments = placeholdUnknownAttachments(apiStatus.MediaAttachments)
|
||||
aside, apiStatus.MediaAttachments = placeholderAttachments(apiStatus.MediaAttachments)
|
||||
apiStatus.Content += aside
|
||||
if apiStatus.Reblog != nil {
|
||||
aside, apiStatus.Reblog.MediaAttachments = placeholdUnknownAttachments(apiStatus.Reblog.MediaAttachments)
|
||||
aside, apiStatus.Reblog.MediaAttachments = placeholderAttachments(apiStatus.Reblog.MediaAttachments)
|
||||
apiStatus.Reblog.Content += aside
|
||||
}
|
||||
|
||||
|
@ -962,15 +937,15 @@ func filterableTextFields(s *gtsmodel.Status) []string {
|
|||
func filterAppliesInContext(filter *gtsmodel.Filter, filterContext statusfilter.FilterContext) bool {
|
||||
switch filterContext {
|
||||
case statusfilter.FilterContextHome:
|
||||
return util.PtrValueOr(filter.ContextHome, false)
|
||||
return util.PtrOrValue(filter.ContextHome, false)
|
||||
case statusfilter.FilterContextNotifications:
|
||||
return util.PtrValueOr(filter.ContextNotifications, false)
|
||||
return util.PtrOrValue(filter.ContextNotifications, false)
|
||||
case statusfilter.FilterContextPublic:
|
||||
return util.PtrValueOr(filter.ContextPublic, false)
|
||||
return util.PtrOrValue(filter.ContextPublic, false)
|
||||
case statusfilter.FilterContextThread:
|
||||
return util.PtrValueOr(filter.ContextThread, false)
|
||||
return util.PtrOrValue(filter.ContextThread, false)
|
||||
case statusfilter.FilterContextAccount:
|
||||
return util.PtrValueOr(filter.ContextAccount, false)
|
||||
return util.PtrOrValue(filter.ContextAccount, false)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -2083,7 +2058,7 @@ func (c *Converter) FilterKeywordToAPIFilterV1(ctx context.Context, filterKeywor
|
|||
ID: filterKeyword.ID,
|
||||
Phrase: filterKeyword.Keyword,
|
||||
Context: filterToAPIFilterContexts(filter),
|
||||
WholeWord: util.PtrValueOr(filterKeyword.WholeWord, false),
|
||||
WholeWord: util.PtrOrValue(filterKeyword.WholeWord, false),
|
||||
ExpiresAt: filterExpiresAtToAPIFilterExpiresAt(filter.ExpiresAt),
|
||||
Irreversible: filter.Action == gtsmodel.FilterActionHide,
|
||||
}, nil
|
||||
|
@ -2121,19 +2096,19 @@ func filterExpiresAtToAPIFilterExpiresAt(expiresAt time.Time) *string {
|
|||
|
||||
func filterToAPIFilterContexts(filter *gtsmodel.Filter) []apimodel.FilterContext {
|
||||
apiContexts := make([]apimodel.FilterContext, 0, apimodel.FilterContextNumValues)
|
||||
if util.PtrValueOr(filter.ContextHome, false) {
|
||||
if util.PtrOrValue(filter.ContextHome, false) {
|
||||
apiContexts = append(apiContexts, apimodel.FilterContextHome)
|
||||
}
|
||||
if util.PtrValueOr(filter.ContextNotifications, false) {
|
||||
if util.PtrOrValue(filter.ContextNotifications, false) {
|
||||
apiContexts = append(apiContexts, apimodel.FilterContextNotifications)
|
||||
}
|
||||
if util.PtrValueOr(filter.ContextPublic, false) {
|
||||
if util.PtrOrValue(filter.ContextPublic, false) {
|
||||
apiContexts = append(apiContexts, apimodel.FilterContextPublic)
|
||||
}
|
||||
if util.PtrValueOr(filter.ContextThread, false) {
|
||||
if util.PtrOrValue(filter.ContextThread, false) {
|
||||
apiContexts = append(apiContexts, apimodel.FilterContextThread)
|
||||
}
|
||||
if util.PtrValueOr(filter.ContextAccount, false) {
|
||||
if util.PtrOrValue(filter.ContextAccount, false) {
|
||||
apiContexts = append(apiContexts, apimodel.FilterContextAccount)
|
||||
}
|
||||
return apiContexts
|
||||
|
@ -2154,7 +2129,7 @@ func (c *Converter) FilterKeywordToAPIFilterKeyword(ctx context.Context, filterK
|
|||
return &apimodel.FilterKeyword{
|
||||
ID: filterKeyword.ID,
|
||||
Keyword: filterKeyword.Keyword,
|
||||
WholeWord: util.PtrValueOr(filterKeyword.WholeWord, false),
|
||||
WholeWord: util.PtrOrValue(filterKeyword.WholeWord, false),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -851,7 +851,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendUnknownAttachments
|
|||
"muted": false,
|
||||
"bookmarked": false,
|
||||
"pinned": false,
|
||||
"content": "\u003cp\u003ehi \u003cspan class=\"h-card\"\u003e\u003ca href=\"http://localhost:8080/@admin\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003e@\u003cspan\u003eadmin\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e here's some media for ya\u003c/p\u003e\u003chr\u003e\u003cp\u003e\u003ci lang=\"en\"\u003eℹ️ Note from localhost:8080: 2 attachments in this status could not be downloaded. Treat the following external links with care:\u003c/i\u003e\u003c/p\u003e\u003cul\u003e\u003cli\u003e\u003ca href=\"http://example.org/fileserver/01HE7Y659ZWZ02JM4AWYJZ176Q/attachment/original/01HE7ZGJYTSYMXF927GF9353KR.svg\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003e01HE7ZGJYTSYMXF927GF9353KR.svg\u003c/a\u003e [SVG line art of a sloth, public domain]\u003c/li\u003e\u003cli\u003e\u003ca href=\"http://example.org/fileserver/01HE7Y659ZWZ02JM4AWYJZ176Q/attachment/original/01HE892Y8ZS68TQCNPX7J888P3.mp3\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003e01HE892Y8ZS68TQCNPX7J888P3.mp3\u003c/a\u003e [Jolly salsa song, public domain.]\u003c/li\u003e\u003c/ul\u003e",
|
||||
"content": "\u003cp\u003ehi \u003cspan class=\"h-card\"\u003e\u003ca href=\"http://localhost:8080/@admin\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003e@\u003cspan\u003eadmin\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e here's some media for ya\u003c/p\u003e\u003chr\u003e\u003chr\u003e\u003cp\u003e\u003ci lang=\"en\"\u003eℹ️ Note from localhost:8080: 2 attachments in this status were not downloaded. Treat the following external links with care:\u003c/i\u003e\u003c/p\u003e\u003cul\u003e\u003cli\u003e\u003ca href=\"http://example.org/fileserver/01HE7Y659ZWZ02JM4AWYJZ176Q/attachment/original/01HE7ZGJYTSYMXF927GF9353KR.svg\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003e01HE7ZGJYTSYMXF927GF9353KR.svg\u003c/a\u003e [SVG line art of a sloth, public domain]\u003c/li\u003e\u003cli\u003e\u003ca href=\"http://example.org/fileserver/01HE7Y659ZWZ02JM4AWYJZ176Q/attachment/original/01HE892Y8ZS68TQCNPX7J888P3.mp3\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"\u003e01HE892Y8ZS68TQCNPX7J888P3.mp3\u003c/a\u003e [Jolly salsa song, public domain.]\u003c/li\u003e\u003c/ul\u003e",
|
||||
"reblog": null,
|
||||
"account": {
|
||||
"id": "01FHMQX3GAABWSM0S2VZEC2SWC",
|
||||
|
@ -1070,30 +1070,30 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
|
|||
{
|
||||
"id": "01HE7ZFX9GKA5ZZVD4FACABSS9",
|
||||
"type": "unknown",
|
||||
"url": "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE7ZFX9GKA5ZZVD4FACABSS9.svg",
|
||||
"text_url": "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE7ZFX9GKA5ZZVD4FACABSS9.svg",
|
||||
"preview_url": "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE7ZFX9GKA5ZZVD4FACABSS9.jpg",
|
||||
"url": null,
|
||||
"text_url": null,
|
||||
"preview_url": null,
|
||||
"remote_url": "http://example.org/fileserver/01HE7Y659ZWZ02JM4AWYJZ176Q/attachment/original/01HE7ZGJYTSYMXF927GF9353KR.svg",
|
||||
"preview_remote_url": null,
|
||||
"meta": null,
|
||||
"description": "SVG line art of a sloth, public domain",
|
||||
"blurhash": "L26*j+~qE1RP?wxut7ofRlM{R*of",
|
||||
"Sensitive": true,
|
||||
"MIMEType": "image/svg"
|
||||
"MIMEType": ""
|
||||
},
|
||||
{
|
||||
"id": "01HE88YG74PVAB81PX2XA9F3FG",
|
||||
"type": "unknown",
|
||||
"url": "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE88YG74PVAB81PX2XA9F3FG.mp3",
|
||||
"text_url": "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE88YG74PVAB81PX2XA9F3FG.mp3",
|
||||
"preview_url": "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE88YG74PVAB81PX2XA9F3FG.jpg",
|
||||
"url": null,
|
||||
"text_url": null,
|
||||
"preview_url": null,
|
||||
"remote_url": "http://example.org/fileserver/01HE7Y659ZWZ02JM4AWYJZ176Q/attachment/original/01HE892Y8ZS68TQCNPX7J888P3.mp3",
|
||||
"preview_remote_url": null,
|
||||
"meta": null,
|
||||
"description": "Jolly salsa song, public domain.",
|
||||
"blurhash": null,
|
||||
"Sensitive": true,
|
||||
"MIMEType": "audio/mpeg"
|
||||
"MIMEType": ""
|
||||
}
|
||||
],
|
||||
"LanguageTag": "en",
|
||||
|
@ -1357,13 +1357,19 @@ func (suite *InternalToFrontendTestSuite) TestVideoAttachmentToFrontend() {
|
|||
"height": 404,
|
||||
"frame_rate": "30/1",
|
||||
"duration": 15.033334,
|
||||
"bitrate": 1206522
|
||||
"bitrate": 1206522,
|
||||
"size": "720x404",
|
||||
"aspect": 1.7821782
|
||||
},
|
||||
"small": {
|
||||
"width": 720,
|
||||
"height": 404,
|
||||
"size": "720x404",
|
||||
"aspect": 1.7821782
|
||||
},
|
||||
"focus": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
"description": "A cow adorably licking another cow!",
|
||||
|
|
|
@ -20,6 +20,7 @@ package typeutils
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/url"
|
||||
"path"
|
||||
"slices"
|
||||
|
@ -35,6 +36,26 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/text"
|
||||
)
|
||||
|
||||
// toAPISize converts a set of media dimensions
|
||||
// to mastodon API compatible size string.
|
||||
func toAPISize(width, height int) string {
|
||||
return strconv.Itoa(width) +
|
||||
"x" +
|
||||
strconv.Itoa(height)
|
||||
}
|
||||
|
||||
// toAPIFrameRate converts a media framerate ptr
|
||||
// to mastodon API compatible framerate string.
|
||||
func toAPIFrameRate(framerate *float32) string {
|
||||
if framerate == nil {
|
||||
return ""
|
||||
}
|
||||
// The masto api expects this as a string in
|
||||
// the format `integer/1`, so 30fps is `30/1`.
|
||||
round := math.Round(float64(*framerate))
|
||||
return strconv.Itoa(int(round)) + "/1"
|
||||
}
|
||||
|
||||
type statusInteractions struct {
|
||||
Favourited bool
|
||||
Muted bool
|
||||
|
@ -92,7 +113,7 @@ func misskeyReportInlineURLs(content string) []*url.URL {
|
|||
return urls
|
||||
}
|
||||
|
||||
// placeholdUnknownAttachments separates any attachments with type `unknown`
|
||||
// placeholderAttachments separates any attachments with missing local URL
|
||||
// out of the given slice, and returns a piece of text containing links to
|
||||
// those attachments, as well as the slice of remaining "known" attachments.
|
||||
// If there are no unknown-type attachments in the provided slice, an empty
|
||||
|
@ -104,62 +125,61 @@ func misskeyReportInlineURLs(content string) []*url.URL {
|
|||
// Example:
|
||||
//
|
||||
// <hr>
|
||||
// <p><i lang="en">ℹ️ Note from your.instance.com: 2 attachments in this status could not be downloaded. Treat the following external links with care:</i></p>
|
||||
// <p><i lang="en">ℹ️ Note from your.instance.com: 2 attachment(s) in this status were not downloaded. Treat the following external link(s) with care:</i></p>
|
||||
// <ul>
|
||||
// <li><a href="http://example.org/fileserver/01HE7Y659ZWZ02JM4AWYJZ176Q/attachment/original/01HE7ZGJYTSYMXF927GF9353KR.svg" rel="nofollow noreferrer noopener" target="_blank">01HE7ZGJYTSYMXF927GF9353KR.svg</a> [SVG line art of a sloth, public domain]</li>
|
||||
// <li><a href="http://example.org/fileserver/01HE7Y659ZWZ02JM4AWYJZ176Q/attachment/original/01HE892Y8ZS68TQCNPX7J888P3.mp3" rel="nofollow noreferrer noopener" target="_blank">01HE892Y8ZS68TQCNPX7J888P3.mp3</a> [Jolly salsa song, public domain.]</li>
|
||||
// </ul>
|
||||
func placeholdUnknownAttachments(arr []*apimodel.Attachment) (string, []*apimodel.Attachment) {
|
||||
// Extract unknown-type attachments into a separate
|
||||
// slice, deleting them from arr in the process.
|
||||
var unknowns []*apimodel.Attachment
|
||||
arr = slices.DeleteFunc(arr, func(elem *apimodel.Attachment) bool {
|
||||
unknown := elem.Type == "unknown"
|
||||
if unknown {
|
||||
// Set aside unknown-type attachment.
|
||||
unknowns = append(unknowns, elem)
|
||||
}
|
||||
func placeholderAttachments(arr []*apimodel.Attachment) (string, []*apimodel.Attachment) {
|
||||
|
||||
return unknown
|
||||
// Extract non-locally stored attachments into a
|
||||
// separate slice, deleting them from input slice.
|
||||
var nonLocal []*apimodel.Attachment
|
||||
arr = slices.DeleteFunc(arr, func(elem *apimodel.Attachment) bool {
|
||||
if elem.URL == nil {
|
||||
nonLocal = append(nonLocal, elem)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
unknownsLen := len(unknowns)
|
||||
if unknownsLen == 0 {
|
||||
// No unknown attachments,
|
||||
// nothing to do.
|
||||
if len(nonLocal) == 0 {
|
||||
// No non-locally
|
||||
// stored media.
|
||||
return "", arr
|
||||
}
|
||||
|
||||
// Plural / singular.
|
||||
var (
|
||||
attachments string
|
||||
links string
|
||||
)
|
||||
|
||||
if unknownsLen == 1 {
|
||||
attachments = "1 attachment"
|
||||
links = "link"
|
||||
} else {
|
||||
attachments = strconv.Itoa(unknownsLen) + " attachments"
|
||||
links = "links"
|
||||
}
|
||||
|
||||
var note strings.Builder
|
||||
note.WriteString(`<hr>`)
|
||||
note.WriteString(`<p><i lang="en">`)
|
||||
note.WriteString(`ℹ️ Note from ` + config.GetHost() + `: ` + attachments + ` in this status could not be downloaded. Treat the following external ` + links + ` with care:`)
|
||||
note.WriteString(`</i></p>`)
|
||||
note.WriteString(`<ul>`)
|
||||
for _, a := range unknowns {
|
||||
var (
|
||||
remoteURL = *a.RemoteURL
|
||||
base = path.Base(remoteURL)
|
||||
entry = fmt.Sprintf(`<a href="%s">%s</a>`, remoteURL, base)
|
||||
)
|
||||
if d := a.Description; d != nil && *d != "" {
|
||||
entry += ` [` + *d + `]`
|
||||
note.WriteString(`<hr><p><i lang="en">ℹ️ Note from `)
|
||||
note.WriteString(config.GetHost())
|
||||
note.WriteString(`: `)
|
||||
note.WriteString(strconv.Itoa(len(nonLocal)))
|
||||
|
||||
if len(nonLocal) > 1 {
|
||||
// Use plural word form.
|
||||
note.WriteString(` attachments in this status were not downloaded. ` +
|
||||
`Treat the following external links with care:`)
|
||||
} else {
|
||||
// Use singular word form.
|
||||
note.WriteString(` attachment in this status was not downloaded. ` +
|
||||
`Treat the following external link with care:`)
|
||||
}
|
||||
note.WriteString(`<li>` + entry + `</li>`)
|
||||
|
||||
note.WriteString(`</i></p><ul>`)
|
||||
for _, a := range nonLocal {
|
||||
note.WriteString(`<li>`)
|
||||
note.WriteString(`<a href="`)
|
||||
note.WriteString(*a.RemoteURL)
|
||||
note.WriteString(`">`)
|
||||
note.WriteString(path.Base(*a.RemoteURL))
|
||||
note.WriteString(`</a>`)
|
||||
if d := a.Description; d != nil && *d != "" {
|
||||
note.WriteString(` [`)
|
||||
note.WriteString(*d)
|
||||
note.WriteString(`]`)
|
||||
}
|
||||
note.WriteString(`</li>`)
|
||||
}
|
||||
note.WriteString(`</ul>`)
|
||||
|
||||
|
|
|
@ -43,10 +43,19 @@ func PtrIf[T comparable](t T) *T {
|
|||
return &t
|
||||
}
|
||||
|
||||
// PtrValueOr returns either value of ptr, or default.
|
||||
func PtrValueOr[T any](t *T, _default T) T {
|
||||
// PtrOrZero returns either value of ptr, or zero.
|
||||
func PtrOrZero[T any](t *T) T {
|
||||
if t == nil {
|
||||
var z T
|
||||
return z
|
||||
}
|
||||
return *t
|
||||
}
|
||||
|
||||
// PtrOrValue returns either contained value of ptr, or 'value'.
|
||||
func PtrOrValue[T any](t *T, value T) T {
|
||||
if t != nil {
|
||||
return *t
|
||||
}
|
||||
return _default
|
||||
return value
|
||||
}
|
||||
|
|
|
@ -1188,17 +1188,8 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
|||
Description: "SVG line art of a sloth, public domain",
|
||||
Blurhash: "L26*j+~qE1RP?wxut7ofRlM{R*of",
|
||||
Processing: 2,
|
||||
File: gtsmodel.File{
|
||||
Path: "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE7ZFX9GKA5ZZVD4FACABSS9.svg",
|
||||
ContentType: "image/svg",
|
||||
FileSize: 147819,
|
||||
},
|
||||
Thumbnail: gtsmodel.Thumbnail{
|
||||
Path: "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE7ZFX9GKA5ZZVD4FACABSS9.jpg",
|
||||
ContentType: "image/jpeg",
|
||||
FileSize: 0,
|
||||
URL: "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE7ZFX9GKA5ZZVD4FACABSS9.jpg",
|
||||
},
|
||||
File: gtsmodel.File{},
|
||||
Thumbnail: gtsmodel.Thumbnail{RemoteURL: ""},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(false),
|
||||
|
@ -1216,17 +1207,8 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
|||
Description: "Jolly salsa song, public domain.",
|
||||
Blurhash: "",
|
||||
Processing: 2,
|
||||
File: gtsmodel.File{
|
||||
Path: "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE88YG74PVAB81PX2XA9F3FG.mp3",
|
||||
ContentType: "audio/mpeg",
|
||||
FileSize: 147819,
|
||||
},
|
||||
Thumbnail: gtsmodel.Thumbnail{
|
||||
Path: "01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE88YG74PVAB81PX2XA9F3FG.jpg",
|
||||
ContentType: "image/jpeg",
|
||||
FileSize: 0,
|
||||
URL: "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/small/01HE88YG74PVAB81PX2XA9F3FG.jpg",
|
||||
},
|
||||
File: gtsmodel.File{},
|
||||
Thumbnail: gtsmodel.Thumbnail{RemoteURL: ""},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(false),
|
||||
|
|
Loading…
Reference in a new issue