2023-03-12 15:00:57 +00:00
// 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/>.
2021-02-28 14:17:18 +00:00
2021-03-09 16:03:40 +00:00
package media
2021-04-01 18:46:45 +00:00
import (
2021-05-17 17:06:58 +00:00
"context"
2023-02-13 18:40:48 +00:00
"errors"
2022-03-07 10:08:26 +00:00
"fmt"
2023-05-28 12:08:35 +00:00
"io"
2023-02-13 18:40:48 +00:00
"time"
2021-04-01 18:46:45 +00:00
2023-05-28 12:08:35 +00:00
"codeberg.org/gruf/go-iotools"
2023-02-13 18:40:48 +00:00
"codeberg.org/gruf/go-store/v2/storage"
2023-06-22 19:46:36 +00:00
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
2023-02-13 18:40:48 +00:00
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/uris"
2021-04-01 18:46:45 +00:00
)
2023-02-11 11:48:38 +00:00
var SupportedMIMETypes = [ ] string {
mimeImageJpeg ,
mimeImageGif ,
mimeImagePng ,
mimeImageWebp ,
mimeVideoMp4 ,
}
2022-05-15 14:45:04 +00:00
2023-02-11 11:48:38 +00:00
var SupportedEmojiMIMETypes = [ ] string {
mimeImageGif ,
mimeImagePng ,
}
2022-06-30 10:22:10 +00:00
2023-05-28 12:08:35 +00:00
type Manager struct {
2023-02-13 18:40:48 +00:00
state * state . State
2021-04-01 18:46:45 +00:00
}
2022-01-10 17:36:09 +00:00
// NewManager returns a media manager with the given db and underlying storage.
//
// A worker pool will also be initialized for the manager, to ensure that only
2022-05-07 15:36:01 +00:00
// a limited number of media will be processed in parallel. The numbers of workers
// is determined from the $GOMAXPROCS environment variable (usually no. CPU cores).
2022-05-15 09:16:43 +00:00
// See internal/concurrency.NewWorkerPool() documentation for further information.
2023-05-28 12:08:35 +00:00
func NewManager ( state * state . State ) * Manager {
m := & Manager { state : state }
2023-02-13 18:40:48 +00:00
return m
}
2023-05-28 12:08:35 +00:00
// PreProcessMedia begins the process of decoding and storing the given data as an attachment.
// It will return a pointer to a ProcessingMedia struct upon which further actions can be performed, such as getting
// the finished media, thumbnail, attachment, etc.
//
// data should be a function that the media manager can call to return a reader containing the media data.
//
// accountID should be the account that the media belongs to.
//
// ai is optional and can be nil. Any additional information about the attachment provided will be put in the database.
//
// Note: unlike ProcessMedia, this will NOT queue the media to be asynchronously processed.
func ( m * Manager ) PreProcessMedia ( ctx context . Context , data DataFunc , accountID string , ai * AdditionalMediaInfo ) ( * ProcessingMedia , error ) {
2023-02-13 18:40:48 +00:00
id , err := id . NewRandomULID ( )
if err != nil {
return nil , err
2022-01-10 17:36:09 +00:00
}
2023-02-13 18:40:48 +00:00
avatar := false
header := false
cached := false
now := time . Now ( )
// populate initial fields on the media attachment -- some of these will be overwritten as we proceed
attachment := & gtsmodel . MediaAttachment {
ID : id ,
CreatedAt : now ,
UpdatedAt : now ,
StatusID : "" ,
URL : "" , // we don't know yet because it depends on the uncalled DataFunc
RemoteURL : "" ,
Type : gtsmodel . FileTypeUnknown , // we don't know yet because it depends on the uncalled DataFunc
FileMeta : gtsmodel . FileMeta { } ,
AccountID : accountID ,
Description : "" ,
ScheduledStatusID : "" ,
Blurhash : "" ,
Processing : gtsmodel . ProcessingStatusReceived ,
File : gtsmodel . File { UpdatedAt : now } ,
Thumbnail : gtsmodel . Thumbnail { UpdatedAt : now } ,
Avatar : & avatar ,
Header : & header ,
Cached : & cached ,
}
// check if we have additional info to add to the attachment,
// and overwrite some of the attachment fields if so
if ai != nil {
if ai . CreatedAt != nil {
attachment . CreatedAt = * ai . CreatedAt
2022-05-07 15:36:01 +00:00
}
2023-02-13 18:40:48 +00:00
if ai . StatusID != nil {
attachment . StatusID = * ai . StatusID
2022-05-07 15:36:01 +00:00
}
2023-02-13 18:40:48 +00:00
if ai . RemoteURL != nil {
attachment . RemoteURL = * ai . RemoteURL
}
if ai . Description != nil {
attachment . Description = * ai . Description
}
if ai . ScheduledStatusID != nil {
attachment . ScheduledStatusID = * ai . ScheduledStatusID
}
if ai . Blurhash != nil {
attachment . Blurhash = * ai . Blurhash
}
if ai . Avatar != nil {
attachment . Avatar = ai . Avatar
}
if ai . Header != nil {
attachment . Header = ai . Header
}
if ai . FocusX != nil {
attachment . FileMeta . Focus . X = * ai . FocusX
}
if ai . FocusY != nil {
attachment . FileMeta . Focus . Y = * ai . FocusY
}
2022-05-07 15:36:01 +00:00
}
2023-02-13 18:40:48 +00:00
processingMedia := & ProcessingMedia {
media : attachment ,
dataFn : data ,
mgr : m ,
2021-04-01 18:46:45 +00:00
}
2022-01-03 16:37:38 +00:00
2023-02-13 18:40:48 +00:00
return processingMedia , nil
}
2023-05-28 12:08:35 +00:00
// PreProcessMediaRecache refetches, reprocesses, and recaches an existing attachment that has been uncached via pruneRemote.
//
// Note: unlike ProcessMedia, this will NOT queue the media to be asychronously processed.
func ( m * Manager ) PreProcessMediaRecache ( ctx context . Context , data DataFunc , attachmentID string ) ( * ProcessingMedia , error ) {
2023-02-13 18:40:48 +00:00
// get the existing attachment from database.
attachment , err := m . state . DB . GetAttachmentByID ( ctx , attachmentID )
if err != nil {
2022-05-15 14:45:04 +00:00
return nil , err
2022-03-07 10:08:26 +00:00
}
2023-02-13 18:40:48 +00:00
processingMedia := & ProcessingMedia {
media : attachment ,
dataFn : data ,
recache : true , // indicate it's a recache
mgr : m ,
}
return processingMedia , nil
2021-04-01 18:46:45 +00:00
}
2023-05-28 12:08:35 +00:00
// ProcessMedia will call PreProcessMedia, followed by queuing the media to be processing in the media worker queue.
func ( m * Manager ) ProcessMedia ( ctx context . Context , data DataFunc , accountID string , ai * AdditionalMediaInfo ) ( * ProcessingMedia , error ) {
2023-02-13 18:40:48 +00:00
// Create a new processing media object for this media request.
2023-05-28 12:08:35 +00:00
media , err := m . PreProcessMedia ( ctx , data , accountID , ai )
2021-12-28 15:36:00 +00:00
if err != nil {
return nil , err
}
2023-02-13 18:40:48 +00:00
// Attempt to add this media processing item to the worker queue.
_ = m . state . Workers . Media . MustEnqueueCtx ( ctx , media . Process )
return media , nil
2022-01-11 16:49:14 +00:00
}
2022-01-03 16:37:38 +00:00
2023-05-28 12:08:35 +00:00
// PreProcessEmoji begins the process of decoding and storing the given data as an emoji.
// It will return a pointer to a ProcessingEmoji struct upon which further actions can be performed, such as getting
// the finished media, thumbnail, attachment, etc.
//
// data should be a function that the media manager can call to return a reader containing the emoji data.
//
// shortcode should be the emoji shortcode without the ':'s around it.
//
// id is the database ID that should be used to store the emoji.
//
// uri is the ActivityPub URI/ID of the emoji.
//
// ai is optional and can be nil. Any additional information about the emoji provided will be put in the database.
//
// Note: unlike ProcessEmoji, this will NOT queue the emoji to be asynchronously processed.
func ( m * Manager ) PreProcessEmoji ( ctx context . Context , data DataFunc , shortcode string , emojiID string , uri string , ai * AdditionalEmojiInfo , refresh bool ) ( * ProcessingEmoji , error ) {
2023-02-13 18:40:48 +00:00
instanceAccount , err := m . state . DB . GetInstanceAccount ( ctx , "" )
2022-01-11 16:49:14 +00:00
if err != nil {
2023-06-22 19:46:36 +00:00
return nil , gtserror . Newf ( "error fetching this instance account from the db: %s" , err )
2023-02-13 18:40:48 +00:00
}
var (
newPathID string
emoji * gtsmodel . Emoji
now = time . Now ( )
)
if refresh {
2023-05-28 12:08:35 +00:00
// Look for existing emoji by given ID.
2023-02-13 18:40:48 +00:00
emoji , err = m . state . DB . GetEmojiByID ( ctx , emojiID )
if err != nil {
2023-06-22 19:46:36 +00:00
return nil , gtserror . Newf ( "error fetching emoji to refresh from the db: %s" , err )
2023-02-13 18:40:48 +00:00
}
// if this is a refresh, we will end up with new images
2023-05-28 12:08:35 +00:00
// stored for this emoji, so we can use an io.Closer callback
2023-02-13 18:40:48 +00:00
// to perform clean up of the old images from storage
2023-05-28 12:08:35 +00:00
originalData := data
2023-02-13 18:40:48 +00:00
originalImagePath := emoji . ImagePath
originalImageStaticPath := emoji . ImageStaticPath
2023-05-28 12:08:35 +00:00
data = func ( ctx context . Context ) ( io . ReadCloser , int64 , error ) {
// Call original data func.
rc , sz , err := originalData ( ctx )
if err != nil {
return nil , 0 , err
2023-02-13 18:40:48 +00:00
}
2023-05-28 12:08:35 +00:00
// Wrap closer to cleanup old data.
c := iotools . CloserCallback ( rc , func ( ) {
if err := m . state . Storage . Delete ( ctx , originalImagePath ) ; err != nil && ! errors . Is ( err , storage . ErrNotFound ) {
log . Errorf ( ctx , "error removing old emoji %s@%s from storage: %v" , emoji . Shortcode , emoji . Domain , err )
}
if err := m . state . Storage . Delete ( ctx , originalImageStaticPath ) ; err != nil && ! errors . Is ( err , storage . ErrNotFound ) {
log . Errorf ( ctx , "error removing old static emoji %s@%s from storage: %v" , emoji . Shortcode , emoji . Domain , err )
}
} )
2023-02-13 18:40:48 +00:00
2023-05-28 12:08:35 +00:00
// Return newly wrapped readcloser and size.
return iotools . ReadCloser ( rc , c ) , sz , nil
2023-02-13 18:40:48 +00:00
}
newPathID , err = id . NewRandomULID ( )
if err != nil {
2023-06-22 19:46:36 +00:00
return nil , gtserror . Newf ( "error generating alternateID for emoji refresh: %s" , err )
2023-02-13 18:40:48 +00:00
}
// store + serve static image at new path ID
emoji . ImageStaticURL = uris . GenerateURIForAttachment ( instanceAccount . ID , string ( TypeEmoji ) , string ( SizeStatic ) , newPathID , mimePng )
emoji . ImageStaticPath = fmt . Sprintf ( "%s/%s/%s/%s.%s" , instanceAccount . ID , TypeEmoji , SizeStatic , newPathID , mimePng )
emoji . Shortcode = shortcode
emoji . URI = uri
} else {
disabled := false
visibleInPicker := true
// populate initial fields on the emoji -- some of these will be overwritten as we proceed
emoji = & gtsmodel . Emoji {
ID : emojiID ,
CreatedAt : now ,
Shortcode : shortcode ,
Domain : "" , // assume our own domain unless told otherwise
ImageRemoteURL : "" ,
ImageStaticRemoteURL : "" ,
ImageURL : "" , // we don't know yet
ImageStaticURL : uris . GenerateURIForAttachment ( instanceAccount . ID , string ( TypeEmoji ) , string ( SizeStatic ) , emojiID , mimePng ) , // all static emojis are encoded as png
ImagePath : "" , // we don't know yet
ImageStaticPath : fmt . Sprintf ( "%s/%s/%s/%s.%s" , instanceAccount . ID , TypeEmoji , SizeStatic , emojiID , mimePng ) , // all static emojis are encoded as png
ImageContentType : "" , // we don't know yet
ImageStaticContentType : mimeImagePng , // all static emojis are encoded as png
ImageFileSize : 0 ,
ImageStaticFileSize : 0 ,
Disabled : & disabled ,
URI : uri ,
VisibleInPicker : & visibleInPicker ,
CategoryID : "" ,
}
}
emoji . ImageUpdatedAt = now
emoji . UpdatedAt = now
// check if we have additional info to add to the emoji,
// and overwrite some of the emoji fields if so
if ai != nil {
if ai . CreatedAt != nil {
emoji . CreatedAt = * ai . CreatedAt
}
if ai . Domain != nil {
emoji . Domain = * ai . Domain
}
if ai . ImageRemoteURL != nil {
emoji . ImageRemoteURL = * ai . ImageRemoteURL
}
if ai . ImageStaticRemoteURL != nil {
emoji . ImageStaticRemoteURL = * ai . ImageStaticRemoteURL
}
if ai . Disabled != nil {
emoji . Disabled = ai . Disabled
}
if ai . VisibleInPicker != nil {
emoji . VisibleInPicker = ai . VisibleInPicker
}
if ai . CategoryID != nil {
emoji . CategoryID = * ai . CategoryID
}
2021-05-21 13:48:26 +00:00
}
2023-02-13 18:40:48 +00:00
processingEmoji := & ProcessingEmoji {
instAccID : instanceAccount . ID ,
emoji : emoji ,
refresh : refresh ,
newPathID : newPathID ,
dataFn : data ,
mgr : m ,
}
2022-01-11 16:49:14 +00:00
return processingEmoji , nil
2022-01-08 16:17:01 +00:00
}
2023-05-28 12:08:35 +00:00
// ProcessEmoji will call PreProcessEmoji, followed by queuing the emoji to be processing in the emoji worker queue.
func ( m * Manager ) ProcessEmoji ( ctx context . Context , data DataFunc , shortcode string , id string , uri string , ai * AdditionalEmojiInfo , refresh bool ) ( * ProcessingEmoji , error ) {
2023-02-13 18:40:48 +00:00
// Create a new processing emoji object for this emoji request.
2023-05-28 12:08:35 +00:00
emoji , err := m . PreProcessEmoji ( ctx , data , shortcode , id , uri , ai , refresh )
2022-03-07 10:08:26 +00:00
if err != nil {
return nil , err
}
2023-02-13 18:40:48 +00:00
// Attempt to add this emoji processing item to the worker queue.
_ = m . state . Workers . Media . MustEnqueueCtx ( ctx , emoji . Process )
return emoji , nil
2022-03-07 10:08:26 +00:00
}