2022-01-11 16:49:14 +00:00
/ *
GoToSocial
Copyright ( C ) 2021 - 2022 GoToSocial Authors admin @ gotosocial . org
This program is free software : you can redistribute it and / or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation , either version 3 of the License , or
( at your option ) any later version .
This program is distributed in the hope that it will be useful ,
but WITHOUT ANY WARRANTY ; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
GNU Affero General Public License for more details .
You should have received a copy of the GNU Affero General Public License
along with this program . If not , see < http : //www.gnu.org/licenses/>.
* /
package media
import (
"context"
"fmt"
"strings"
"sync"
"time"
"codeberg.org/gruf/go-store/kv"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/uris"
)
// ProcessingEmoji represents an emoji currently processing. It exposes
// various functions for retrieving data from the process.
type ProcessingEmoji struct {
mu sync . Mutex
2022-01-11 16:51:45 +00:00
// id of this instance's account -- pinned for convenience here so we only need to fetch it once
instanceAccountID string
2022-01-11 16:49:14 +00:00
/ *
below fields should be set on newly created media ;
emoji will be updated incrementally as media goes through processing
* /
emoji * gtsmodel . Emoji
data DataFunc
rawData [ ] byte // will be set once the fetchRawData function has been called
/ *
below fields represent the processing state of the static version of the emoji
* /
staticState processState
static * ImageMeta
/ *
below fields represent the processing state of the emoji image
* /
fullSizeState processState
fullSize * ImageMeta
/ *
below pointers to database and storage are maintained so that
the media can store and update itself during processing steps
* /
database db . DB
storage * kv . KVStore
err error // error created during processing, if any
2022-01-15 16:36:15 +00:00
// track whether this emoji has already been put in the databse
insertedInDB bool
2022-01-11 16:49:14 +00:00
}
// EmojiID returns the ID of the underlying emoji without blocking processing.
func ( p * ProcessingEmoji ) EmojiID ( ) string {
return p . emoji . ID
}
// LoadEmoji blocks until the static and fullsize image
// has been processed, and then returns the completed emoji.
func ( p * ProcessingEmoji ) LoadEmoji ( ctx context . Context ) ( * gtsmodel . Emoji , error ) {
if err := p . fetchRawData ( ctx ) ; err != nil {
return nil , err
}
if _ , err := p . loadStatic ( ctx ) ; err != nil {
return nil , err
}
if _ , err := p . loadFullSize ( ctx ) ; err != nil {
return nil , err
}
2022-01-15 16:36:15 +00:00
// store the result in the database before returning it
p . mu . Lock ( )
defer p . mu . Unlock ( )
if ! p . insertedInDB {
if err := p . database . Put ( ctx , p . emoji ) ; err != nil {
return nil , err
}
p . insertedInDB = true
}
2022-01-11 16:49:14 +00:00
return p . emoji , nil
}
// Finished returns true if processing has finished for both the thumbnail
// and full fized version of this piece of media.
func ( p * ProcessingEmoji ) Finished ( ) bool {
return p . staticState == complete && p . fullSizeState == complete
}
func ( p * ProcessingEmoji ) loadStatic ( ctx context . Context ) ( * ImageMeta , error ) {
p . mu . Lock ( )
defer p . mu . Unlock ( )
switch p . staticState {
case received :
// we haven't processed a static version of this emoji yet so do it now
static , err := deriveStaticEmoji ( p . rawData , p . emoji . ImageContentType )
if err != nil {
p . err = fmt . Errorf ( "error deriving static: %s" , err )
p . staticState = errored
return nil , p . err
}
// put the static in storage
if err := p . storage . Put ( p . emoji . ImageStaticPath , static . image ) ; err != nil {
p . err = fmt . Errorf ( "error storing static: %s" , err )
p . staticState = errored
return nil , p . err
}
// set appropriate fields on the emoji based on the static version we derived
2022-01-15 13:33:58 +00:00
p . emoji . ImageStaticFileSize = len ( static . image )
2022-01-11 16:49:14 +00:00
2022-01-15 13:33:58 +00:00
// set the static on the processing emoji
p . static = static
2022-01-11 16:49:14 +00:00
2022-01-15 13:33:58 +00:00
// we're done processing the static version of the emoji!
p . staticState = complete
2022-01-11 16:49:14 +00:00
fallthrough
case complete :
2022-01-15 13:33:58 +00:00
return p . static , nil
2022-01-11 16:49:14 +00:00
case errored :
return nil , p . err
}
2022-01-15 13:33:58 +00:00
return nil , fmt . Errorf ( "static processing status %d unknown" , p . staticState )
2022-01-11 16:49:14 +00:00
}
func ( p * ProcessingEmoji ) loadFullSize ( ctx context . Context ) ( * ImageMeta , error ) {
p . mu . Lock ( )
defer p . mu . Unlock ( )
switch p . fullSizeState {
case received :
var err error
var decoded * ImageMeta
2022-01-15 13:33:58 +00:00
ct := p . emoji . ImageContentType
2022-01-11 16:49:14 +00:00
switch ct {
2022-01-15 13:33:58 +00:00
case mimeImagePng :
decoded , err = decodeImage ( p . rawData , ct )
2022-01-11 16:49:14 +00:00
case mimeImageGif :
2022-01-15 13:33:58 +00:00
decoded , err = decodeGif ( p . rawData )
2022-01-11 16:49:14 +00:00
default :
2022-01-15 13:33:58 +00:00
err = fmt . Errorf ( "content type %s not a processible emoji type" , ct )
2022-01-11 16:49:14 +00:00
}
if err != nil {
p . err = err
p . fullSizeState = errored
return nil , err
}
2022-01-15 13:33:58 +00:00
// put the full size emoji in storage
if err := p . storage . Put ( p . emoji . ImagePath , decoded . image ) ; err != nil {
p . err = fmt . Errorf ( "error storing full size emoji: %s" , err )
2022-01-11 16:49:14 +00:00
p . fullSizeState = errored
return nil , p . err
}
// set the fullsize of this media
p . fullSize = decoded
2022-01-15 13:33:58 +00:00
// we're done processing the full-size emoji
2022-01-11 16:49:14 +00:00
p . fullSizeState = complete
fallthrough
case complete :
return p . fullSize , nil
case errored :
return nil , p . err
}
return nil , fmt . Errorf ( "full size processing status %d unknown" , p . fullSizeState )
}
// fetchRawData calls the data function attached to p if it hasn't been called yet,
2022-01-15 16:36:15 +00:00
// and updates the underlying emoji fields as necessary.
2022-01-11 16:49:14 +00:00
// It should only be called from within a function that already has a lock on p!
func ( p * ProcessingEmoji ) fetchRawData ( ctx context . Context ) error {
// check if we've already done this and bail early if we have
if p . rawData != nil {
return nil
}
// execute the data function and pin the raw bytes for further processing
b , err := p . data ( ctx )
if err != nil {
return fmt . Errorf ( "fetchRawData: error executing data function: %s" , err )
}
p . rawData = b
// now we have the data we can work out the content type
contentType , err := parseContentType ( p . rawData )
if err != nil {
return fmt . Errorf ( "fetchRawData: error parsing content type: %s" , err )
}
if ! supportedEmoji ( contentType ) {
return fmt . Errorf ( "fetchRawData: content type %s was not valid for an emoji" , contentType )
}
split := strings . Split ( contentType , "/" )
extension := split [ 1 ] // something like 'gif'
// set some additional fields on the emoji now that
// we know more about what the underlying image actually is
2022-01-15 13:33:58 +00:00
p . emoji . ImageURL = uris . GenerateURIForAttachment ( p . instanceAccountID , string ( TypeEmoji ) , string ( SizeOriginal ) , p . emoji . ID , extension )
p . emoji . ImagePath = fmt . Sprintf ( "%s/%s/%s/%s.%s" , p . instanceAccountID , TypeEmoji , SizeOriginal , p . emoji . ID , extension )
p . emoji . ImageContentType = contentType
p . emoji . ImageFileSize = len ( p . rawData )
2022-01-11 16:49:14 +00:00
return nil
}
2022-01-15 13:33:58 +00:00
func ( m * manager ) preProcessEmoji ( ctx context . Context , data DataFunc , shortcode string , id string , uri string , ai * AdditionalEmojiInfo ) ( * ProcessingEmoji , error ) {
2022-01-11 16:49:14 +00:00
instanceAccount , err := m . db . GetInstanceAccount ( ctx , "" )
if err != nil {
return nil , fmt . Errorf ( "preProcessEmoji: error fetching this instance account from the db: %s" , err )
}
// populate initial fields on the emoji -- some of these will be overwritten as we proceed
emoji := & gtsmodel . Emoji {
ID : id ,
CreatedAt : time . Now ( ) ,
UpdatedAt : time . 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 ) , id , 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 , id , 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 ,
ImageUpdatedAt : time . Now ( ) ,
Disabled : false ,
2022-01-15 13:33:58 +00:00
URI : uri ,
2022-01-11 16:49:14 +00:00
VisibleInPicker : true ,
CategoryID : "" ,
}
// 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 {
2022-01-15 13:33:58 +00:00
emoji . CreatedAt = * ai . CreatedAt
2022-01-11 16:49:14 +00:00
}
2022-01-15 13:33:58 +00:00
if ai . Domain != nil {
emoji . Domain = * ai . Domain
2022-01-11 16:49:14 +00:00
}
2022-01-15 13:33:58 +00:00
if ai . ImageRemoteURL != nil {
emoji . ImageRemoteURL = * ai . ImageRemoteURL
2022-01-11 16:49:14 +00:00
}
2022-01-15 13:33:58 +00:00
if ai . ImageStaticRemoteURL != nil {
emoji . ImageStaticRemoteURL = * ai . ImageStaticRemoteURL
2022-01-11 16:49:14 +00:00
}
2022-01-15 13:33:58 +00:00
if ai . Disabled != nil {
emoji . Disabled = * ai . Disabled
2022-01-11 16:49:14 +00:00
}
2022-01-15 13:33:58 +00:00
if ai . VisibleInPicker != nil {
emoji . VisibleInPicker = * ai . VisibleInPicker
2022-01-11 16:49:14 +00:00
}
2022-01-15 13:33:58 +00:00
if ai . CategoryID != nil {
emoji . CategoryID = * ai . CategoryID
2022-01-11 16:49:14 +00:00
}
}
processingEmoji := & ProcessingEmoji {
2022-01-11 16:51:45 +00:00
instanceAccountID : instanceAccount . ID ,
emoji : emoji ,
data : data ,
staticState : received ,
fullSizeState : received ,
database : m . db ,
storage : m . storage ,
2022-01-11 16:49:14 +00:00
}
return processingEmoji , nil
}