From 72ba5666a6ffd06ccdfd2db8dacc47de7f777a4c Mon Sep 17 00:00:00 2001
From: kim <89579420+NyaaaWhatsUpDoc@users.noreply.github.com>
Date: Wed, 17 Jul 2024 15:26:33 +0000
Subject: [PATCH] [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
---
cmd/process-emoji/main.go | 3 +
cmd/process-media/main.go | 3 +
internal/api/client/accounts/mute.go | 2 +-
internal/api/client/filters/v1/validate.go | 4 +-
.../filters/v2/filterkeywordget_test.go | 2 +-
.../client/filters/v2/filterkeywordpost.go | 2 +-
internal/api/client/filters/v2/filterpost.go | 4 +-
internal/api/client/filters/v2/filterput.go | 4 +-
.../20240318115336_account_settings.go | 6 +-
...40715204203_media_pipeline_improvements.go | 124 +++++++++++++
.../emoji.go | 65 +++++++
.../media.go | 127 +++++++++++++
internal/gtsmodel/mediaattachment.go | 34 +++-
internal/media/ffmpeg/pool.go | 31 +++-
internal/media/manager.go | 171 +++---------------
internal/media/manager_test.go | 8 +-
internal/media/processingemoji.go | 57 +++---
internal/media/processingmedia.go | 36 +++-
internal/processing/account/follow.go | 4 +-
internal/processing/admin/emoji.go | 4 +-
internal/processing/filters/v1/create.go | 2 +-
internal/processing/filters/v1/update.go | 12 +-
internal/processing/filters/v2/update.go | 2 +-
internal/storage/storage.go | 9 +-
internal/typeutils/internaltofrontend.go | 165 +++++++----------
internal/typeutils/internaltofrontend_test.go | 26 ++-
internal/typeutils/util.go | 100 ++++++----
internal/util/ptr.go | 15 +-
testrig/testmodels.go | 38 +---
29 files changed, 665 insertions(+), 395 deletions(-)
create mode 100644 internal/db/bundb/migrations/20240715204203_media_pipeline_improvements.go
create mode 100644 internal/db/bundb/migrations/20240715204203_media_pipeline_improvements/emoji.go
create mode 100644 internal/db/bundb/migrations/20240715204203_media_pipeline_improvements/media.go
diff --git a/cmd/process-emoji/main.go b/cmd/process-emoji/main.go
index b06eb84f82..0e999503e2 100644
--- a/cmd/process-emoji/main.go
+++ b/cmd/process-emoji/main.go
@@ -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 ")
}
diff --git a/cmd/process-media/main.go b/cmd/process-media/main.go
index 096d718f90..7487917bfa 100644
--- a/cmd/process-media/main.go
+++ b/cmd/process-media/main.go
@@ -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 ")
}
diff --git a/internal/api/client/accounts/mute.go b/internal/api/client/accounts/mute.go
index 37cd3bbff5..affb0f0557 100644
--- a/internal/api/client/accounts/mute.go
+++ b/internal/api/client/accounts/mute.go
@@ -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
diff --git a/internal/api/client/filters/v1/validate.go b/internal/api/client/filters/v1/validate.go
index 550df54fa8..cce00fdc4e 100644
--- a/internal/api/client/filters/v1/validate.go
+++ b/internal/api/client/filters/v1/validate.go
@@ -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")
diff --git a/internal/api/client/filters/v2/filterkeywordget_test.go b/internal/api/client/filters/v2/filterkeywordget_test.go
index a5d8754a6e..13e90c0c2c 100644
--- a/internal/api/client/filters/v2/filterkeywordget_test.go
+++ b/internal/api/client/filters/v2/filterkeywordget_test.go
@@ -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() {
diff --git a/internal/api/client/filters/v2/filterkeywordpost.go b/internal/api/client/filters/v2/filterkeywordpost.go
index fab7dc8122..ba8f801355 100644
--- a/internal/api/client/filters/v2/filterkeywordpost.go
+++ b/internal/api/client/filters/v2/filterkeywordpost.go
@@ -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
}
diff --git a/internal/api/client/filters/v2/filterpost.go b/internal/api/client/filters/v2/filterpost.go
index 732b810413..13270b1e58 100644
--- a/internal/api/client/filters/v2/filterpost.go
+++ b/internal/api/client/filters/v2/filterpost.go
@@ -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 {
diff --git a/internal/api/client/filters/v2/filterput.go b/internal/api/client/filters/v2/filterput.go
index cc3531838f..c86dc36dc0 100644
--- a/internal/api/client/filters/v2/filterput.go
+++ b/internal/api/client/filters/v2/filterput.go
@@ -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 {
diff --git a/internal/db/bundb/migrations/20240318115336_account_settings.go b/internal/db/bundb/migrations/20240318115336_account_settings.go
index 25c64e826c..3bf58e21e7 100644
--- a/internal/db/bundb/migrations/20240318115336_account_settings.go
+++ b/internal/db/bundb/migrations/20240318115336_account_settings.go
@@ -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.
diff --git a/internal/db/bundb/migrations/20240715204203_media_pipeline_improvements.go b/internal/db/bundb/migrations/20240715204203_media_pipeline_improvements.go
new file mode 100644
index 0000000000..5f01f53ef2
--- /dev/null
+++ b/internal/db/bundb/migrations/20240715204203_media_pipeline_improvements.go
@@ -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 .
+
+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)
+ }
+}
diff --git a/internal/db/bundb/migrations/20240715204203_media_pipeline_improvements/emoji.go b/internal/db/bundb/migrations/20240715204203_media_pipeline_improvements/emoji.go
new file mode 100644
index 0000000000..f4567ab252
--- /dev/null
+++ b/internal/db/bundb/migrations/20240715204203_media_pipeline_improvements/emoji.go
@@ -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 .
+
+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
+}
diff --git a/internal/db/bundb/migrations/20240715204203_media_pipeline_improvements/media.go b/internal/db/bundb/migrations/20240715204203_media_pipeline_improvements/media.go
new file mode 100644
index 0000000000..471a5abd11
--- /dev/null
+++ b/internal/db/bundb/migrations/20240715204203_media_pipeline_improvements/media.go
@@ -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 .
+
+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
+}
diff --git a/internal/gtsmodel/mediaattachment.go b/internal/gtsmodel/mediaattachment.go
index 471a5abd11..eb792ae3b4 100644
--- a/internal/gtsmodel/mediaattachment.go
+++ b/internal/gtsmodel/mediaattachment.go
@@ -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_"`
diff --git a/internal/media/ffmpeg/pool.go b/internal/media/ffmpeg/pool.go
index 9f6446be36..e63b10e697 100644
--- a/internal/media/ffmpeg/pool.go
+++ b/internal/media/ffmpeg/pool.go
@@ -34,14 +34,33 @@ type wasmInstancePool struct {
}
func (p *wasmInstancePool) Init(ctx context.Context, sz int) error {
+ // Initialize for first time
+ // to preload module into the
+ // wazero compilation cache.
+ inst, err := p.inst.New(ctx)
+ if err != nil {
+ return err
+ }
+
+ // Clamp to 1.
+ if sz <= 0 {
+ sz = 1
+ }
+
+ // Allocate new pool instance channel.
p.pool = make(chan *wasm.Instance, sz)
- for i := 0; i < sz; i++ {
- inst, err := p.inst.New(ctx)
- if err != nil {
- return err
- }
- p.pool <- inst
+
+ // 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
}
diff --git a/internal/media/manager.go b/internal/media/manager.go
index 82b066edc3..13bcebe795 100644
--- a/internal/media/manager.go
+++ b/internal/media/manager.go
@@ -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),
+ Avatar: util.Ptr(false),
+ Header: util.Ptr(false),
+ Cached: util.Ptr(false),
+ CreatedAt: now,
+ UpdatedAt: now,
}
// Check if we were provided additional info
@@ -252,56 +197,23 @@ 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.
emoji := >smodel.Emoji{
- ID: id,
- Shortcode: shortcode,
- Domain: domain,
- ImageStaticURL: staticURL,
- ImageStaticPath: staticPath,
- ImageStaticContentType: "image/png",
- Disabled: util.Ptr(false),
- VisibleInPicker: util.Ptr(true),
- CreatedAt: now,
- UpdatedAt: now,
+ ID: id,
+ Shortcode: shortcode,
+ Domain: domain,
+ Disabled: util.Ptr(false),
+ VisibleInPicker: util.Ptr(true),
+ CreatedAt: now,
+ UpdatedAt: now,
}
// Finally, create new emoji.
@@ -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,9 +355,10 @@ func (m *Manager) createEmoji(
// Return wrapped emoji for later processing.
processingEmoji := &ProcessingEmoji{
- emoji: emoji,
- dataFn: data,
- mgr: m,
+ instAccID: instanceAcc.ID,
+ emoji: emoji,
+ dataFn: data,
+ mgr: m,
}
return processingEmoji, nil
diff --git a/internal/media/manager_test.go b/internal/media/manager_test.go
index 24e0ddd1e4..c908b2994b 100644
--- a/internal/media/manager_test.go
+++ b/internal/media/manager_test.go
@@ -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)
diff --git a/internal/media/processingemoji.go b/internal/media/processingemoji.go
index 996a3aa037..f4265759b7 100644
--- a/internal/media/processingemoji.go
+++ b/internal/media/processingemoji.go
@@ -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
-}
diff --git a/internal/media/processingmedia.go b/internal/media/processingmedia.go
index e5af46a2f1..393f7d7154 100644
--- a/internal/media/processingmedia.go
+++ b/internal/media/processingmedia.go
@@ -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
diff --git a/internal/processing/account/follow.go b/internal/processing/account/follow.go
index 6c066d6a6d..59de8834bb 100644
--- a/internal/processing/account/follow.go
+++ b/internal/processing/account/follow.go
@@ -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.
diff --git a/internal/processing/admin/emoji.go b/internal/processing/admin/emoji.go
index cf5bacef89..66193ccfe3 100644
--- a/internal/processing/admin/emoji.go
+++ b/internal/processing/admin/emoji.go
@@ -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 {
diff --git a/internal/processing/filters/v1/create.go b/internal/processing/filters/v1/create.go
index 4d8ffc3e11..18367dfcec 100644
--- a/internal/processing/filters/v1/create.go
+++ b/internal/processing/filters/v1/create.go
@@ -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}
diff --git a/internal/processing/filters/v1/update.go b/internal/processing/filters/v1/update.go
index 2c2fe5574e..81340b4be6 100644
--- a/internal/processing/filters/v1/update.go
+++ b/internal/processing/filters/v1/update.go
@@ -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}
diff --git a/internal/processing/filters/v2/update.go b/internal/processing/filters/v2/update.go
index d8297de382..0d443d58e0 100644
--- a/internal/processing/filters/v2/update.go
+++ b/internal/processing/filters/v2/update.go
@@ -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.
diff --git a/internal/storage/storage.go b/internal/storage/storage.go
index d05fe35199..508433c1ae 100644
--- a/internal/storage/storage.go
+++ b/internal/storage/storage.go
@@ -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
diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go
index 6350f32697..f11c4af21e 100644
--- a/internal/typeutils/internaltofrontend.go
+++ b/internal/typeutils/internaltofrontend.go
@@ -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)),
+ }
+
+ // 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),
+ }
- // 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 thumbnail file URL.
+ api.PreviewURL = util.Ptr(media.Thumbnail.URL)
}
}
- if i := a.Blurhash; i != "" {
- apiAttachment.Blurhash = &i
- }
-
- 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
- }
+ // 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.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),
}
}
diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go
index 9fd4cea463..e9f53e100f 100644
--- a/internal/typeutils/internaltofrontend_test.go
+++ b/internal/typeutils/internaltofrontend_test.go
@@ -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!",
diff --git a/internal/typeutils/util.go b/internal/typeutils/util.go
index d674bc150f..f28cd25541 100644
--- a/internal/typeutils/util.go
+++ b/internal/typeutils/util.go
@@ -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:
//
//
-// ℹ️ Note from your.instance.com: 2 attachments in this status could not be downloaded. Treat the following external links with care:
+// ℹ️ Note from your.instance.com: 2 attachment(s) in this status were not downloaded. Treat the following external link(s) with care:
//
-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
+func placeholderAttachments(arr []*apimodel.Attachment) (string, []*apimodel.Attachment) {
+
+ // 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 {
- unknown := elem.Type == "unknown"
- if unknown {
- // Set aside unknown-type attachment.
- unknowns = append(unknowns, elem)
+ if elem.URL == nil {
+ nonLocal = append(nonLocal, elem)
+ return true
}
-
- return unknown
+ 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
- )
+ var note strings.Builder
+ note.WriteString(`
`)
+ note.WriteString(`
ℹ️ Note from `)
+ note.WriteString(config.GetHost())
+ note.WriteString(`: `)
+ note.WriteString(strconv.Itoa(len(nonLocal)))
- if unknownsLen == 1 {
- attachments = "1 attachment"
- links = "link"
+ 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 {
- attachments = strconv.Itoa(unknownsLen) + " attachments"
- links = "links"
+ // Use singular word form.
+ note.WriteString(` attachment in this status was not downloaded. ` +
+ `Treat the following external link with care:`)
}
- var note strings.Builder
- note.WriteString(`
`)
- note.WriteString(`
`)
- note.WriteString(`ℹ️ Note from ` + config.GetHost() + `: ` + attachments + ` in this status could not be downloaded. Treat the following external ` + links + ` with care:`)
- note.WriteString(`
`)
- note.WriteString(``)
- for _, a := range unknowns {
- var (
- remoteURL = *a.RemoteURL
- base = path.Base(remoteURL)
- entry = fmt.Sprintf(`%s`, remoteURL, base)
- )
+ note.WriteString(`
`)
+ for _, a := range nonLocal {
+ note.WriteString(`- `)
+ note.WriteString(``)
+ note.WriteString(path.Base(*a.RemoteURL))
+ note.WriteString(``)
if d := a.Description; d != nil && *d != "" {
- entry += ` [` + *d + `]`
+ note.WriteString(` [`)
+ note.WriteString(*d)
+ note.WriteString(`]`)
}
- note.WriteString(`
- ` + entry + `
`)
+ note.WriteString(``)
}
note.WriteString(`
`)
diff --git a/internal/util/ptr.go b/internal/util/ptr.go
index d7c30da858..8a89666c44 100644
--- a/internal/util/ptr.go
+++ b/internal/util/ptr.go
@@ -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
}
diff --git a/testrig/testmodels.go b/testrig/testmodels.go
index efd4785a50..c0cf47b818 100644
--- a/testrig/testmodels.go
+++ b/testrig/testmodels.go
@@ -1188,20 +1188,11 @@ 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",
- },
- Avatar: util.Ptr(false),
- Header: util.Ptr(false),
- Cached: util.Ptr(false),
+ File: gtsmodel.File{},
+ Thumbnail: gtsmodel.Thumbnail{RemoteURL: ""},
+ Avatar: util.Ptr(false),
+ Header: util.Ptr(false),
+ Cached: util.Ptr(false),
},
"remote_account_2_status_1_attachment_3": {
ID: "01HE88YG74PVAB81PX2XA9F3FG",
@@ -1216,20 +1207,11 @@ 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",
- },
- Avatar: util.Ptr(false),
- Header: util.Ptr(false),
- Cached: util.Ptr(false),
+ File: gtsmodel.File{},
+ Thumbnail: gtsmodel.Thumbnail{RemoteURL: ""},
+ Avatar: util.Ptr(false),
+ Header: util.Ptr(false),
+ Cached: util.Ptr(false),
},
}
}