2021-08-25 13:34:33 +00:00
/ *
GoToSocial
2023-01-05 11:43:00 +00:00
Copyright ( C ) 2021 - 2023 GoToSocial Authors admin @ gotosocial . org
2021-08-25 13:34:33 +00:00
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-06-13 16:42:28 +00:00
package status
import (
2021-08-25 13:34:33 +00:00
"context"
2021-06-13 16:42:28 +00:00
"errors"
"fmt"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
2022-11-05 12:33:38 +00:00
"github.com/superseriousbusiness/gotosocial/internal/config"
2021-06-13 16:42:28 +00:00
"github.com/superseriousbusiness/gotosocial/internal/db"
2022-06-08 18:38:03 +00:00
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
2021-06-13 16:42:28 +00:00
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
2023-02-03 10:58:58 +00:00
"github.com/superseriousbusiness/gotosocial/internal/text"
2021-06-13 16:42:28 +00:00
)
2021-08-25 13:34:33 +00:00
func ( p * processor ) ProcessVisibility ( ctx context . Context , form * apimodel . AdvancedStatusCreateForm , accountDefaultVis gtsmodel . Visibility , status * gtsmodel . Status ) error {
2021-06-13 16:42:28 +00:00
// by default all flags are set to true
2021-09-09 14:15:25 +00:00
federated := true
boostable := true
replyable := true
likeable := true
2021-06-13 16:42:28 +00:00
2021-08-02 17:06:44 +00:00
// If visibility isn't set on the form, then just take the account default.
2021-06-13 16:42:28 +00:00
// If that's also not set, take the default for the whole instance.
2021-11-22 07:46:19 +00:00
var vis gtsmodel . Visibility
switch {
case form . Visibility != "" :
2021-10-04 13:24:19 +00:00
vis = p . tc . APIVisToVis ( form . Visibility )
2021-11-22 07:46:19 +00:00
case accountDefaultVis != "" :
2021-08-02 17:06:44 +00:00
vis = accountDefaultVis
2021-11-22 07:46:19 +00:00
default :
2021-08-02 17:06:44 +00:00
vis = gtsmodel . VisibilityDefault
2021-06-13 16:42:28 +00:00
}
2021-08-02 17:06:44 +00:00
switch vis {
2021-06-13 16:42:28 +00:00
case gtsmodel . VisibilityPublic :
// for public, there's no need to change any of the advanced flags from true regardless of what the user filled out
break
case gtsmodel . VisibilityUnlocked :
// for unlocked the user can set any combination of flags they like so look at them all to see if they're set and then apply them
if form . Federated != nil {
2021-09-09 14:15:25 +00:00
federated = * form . Federated
2021-06-13 16:42:28 +00:00
}
if form . Boostable != nil {
2021-09-09 14:15:25 +00:00
boostable = * form . Boostable
2021-06-13 16:42:28 +00:00
}
if form . Replyable != nil {
2021-09-09 14:15:25 +00:00
replyable = * form . Replyable
2021-06-13 16:42:28 +00:00
}
if form . Likeable != nil {
2021-09-09 14:15:25 +00:00
likeable = * form . Likeable
2021-06-13 16:42:28 +00:00
}
case gtsmodel . VisibilityFollowersOnly , gtsmodel . VisibilityMutualsOnly :
// for followers or mutuals only, boostable will *always* be false, but the other fields can be set so check and apply them
2021-09-09 14:15:25 +00:00
boostable = false
2021-06-13 16:42:28 +00:00
if form . Federated != nil {
2021-09-09 14:15:25 +00:00
federated = * form . Federated
2021-06-13 16:42:28 +00:00
}
if form . Replyable != nil {
2021-09-09 14:15:25 +00:00
replyable = * form . Replyable
2021-06-13 16:42:28 +00:00
}
if form . Likeable != nil {
2021-09-09 14:15:25 +00:00
likeable = * form . Likeable
2021-06-13 16:42:28 +00:00
}
case gtsmodel . VisibilityDirect :
// direct is pretty easy: there's only one possible setting so return it
2021-09-09 14:15:25 +00:00
federated = true
boostable = false
replyable = true
likeable = true
2021-06-13 16:42:28 +00:00
}
2021-08-02 17:06:44 +00:00
status . Visibility = vis
2022-08-15 10:35:05 +00:00
status . Federated = & federated
status . Boostable = & boostable
status . Replyable = & replyable
status . Likeable = & likeable
2021-06-13 16:42:28 +00:00
return nil
}
2022-06-08 18:38:03 +00:00
func ( p * processor ) ProcessReplyToID ( ctx context . Context , form * apimodel . AdvancedStatusCreateForm , thisAccountID string , status * gtsmodel . Status ) gtserror . WithCode {
2021-06-13 16:42:28 +00:00
if form . InReplyToID == "" {
return nil
}
// If this status is a reply to another status, we need to do a bit of work to establish whether or not this status can be posted:
//
// 1. Does the replied status exist in the database?
// 2. Is the replied status marked as replyable?
// 3. Does a block exist between either the current account or the account that posted the status it's replying to?
//
// If this is all OK, then we fetch the repliedStatus and the repliedAccount for later processing.
repliedStatus := & gtsmodel . Status { }
repliedAccount := & gtsmodel . Account { }
2022-06-08 18:38:03 +00:00
2021-08-25 13:34:33 +00:00
if err := p . db . GetByID ( ctx , form . InReplyToID , repliedStatus ) ; err != nil {
2021-08-20 10:26:56 +00:00
if err == db . ErrNoEntries {
2022-06-08 18:38:03 +00:00
err := fmt . Errorf ( "status with id %s not replyable because it doesn't exist" , form . InReplyToID )
return gtserror . NewErrorBadRequest ( err , err . Error ( ) )
2021-06-13 16:42:28 +00:00
}
2022-06-08 18:38:03 +00:00
err := fmt . Errorf ( "db error fetching status with id %s: %s" , form . InReplyToID , err )
return gtserror . NewErrorInternalError ( err )
2021-06-13 16:42:28 +00:00
}
2022-08-15 10:35:05 +00:00
if ! * repliedStatus . Replyable {
2022-06-08 18:38:03 +00:00
err := fmt . Errorf ( "status with id %s is marked as not replyable" , form . InReplyToID )
return gtserror . NewErrorForbidden ( err , err . Error ( ) )
2021-06-13 16:42:28 +00:00
}
2021-08-25 13:34:33 +00:00
if err := p . db . GetByID ( ctx , repliedStatus . AccountID , repliedAccount ) ; err != nil {
2021-08-20 10:26:56 +00:00
if err == db . ErrNoEntries {
2022-06-08 18:38:03 +00:00
err := fmt . Errorf ( "status with id %s not replyable because account id %s is not known" , form . InReplyToID , repliedStatus . AccountID )
return gtserror . NewErrorBadRequest ( err , err . Error ( ) )
2021-06-13 16:42:28 +00:00
}
2022-06-08 18:38:03 +00:00
err := fmt . Errorf ( "db error fetching account with id %s: %s" , repliedStatus . AccountID , err )
return gtserror . NewErrorInternalError ( err )
2021-06-13 16:42:28 +00:00
}
2022-06-08 18:38:03 +00:00
2021-08-25 13:34:33 +00:00
if blocked , err := p . db . IsBlocked ( ctx , thisAccountID , repliedAccount . ID , true ) ; err != nil {
2022-06-08 18:38:03 +00:00
err := fmt . Errorf ( "db error checking block: %s" , err )
return gtserror . NewErrorInternalError ( err )
2021-06-13 16:42:28 +00:00
} else if blocked {
2022-06-08 18:38:03 +00:00
err := fmt . Errorf ( "status with id %s not replyable" , form . InReplyToID )
return gtserror . NewErrorNotFound ( err )
2021-06-13 16:42:28 +00:00
}
2022-06-08 18:38:03 +00:00
2021-06-13 16:42:28 +00:00
status . InReplyToID = repliedStatus . ID
2022-08-27 09:35:31 +00:00
status . InReplyToURI = repliedStatus . URI
2021-06-13 16:42:28 +00:00
status . InReplyToAccountID = repliedAccount . ID
return nil
}
2022-11-05 12:33:38 +00:00
func ( p * processor ) ProcessMediaIDs ( ctx context . Context , form * apimodel . AdvancedStatusCreateForm , thisAccountID string , status * gtsmodel . Status ) gtserror . WithCode {
2021-06-13 16:42:28 +00:00
if form . MediaIDs == nil {
return nil
}
2022-10-08 11:50:48 +00:00
attachments := [ ] * gtsmodel . MediaAttachment { }
attachmentIDs := [ ] string { }
2021-06-13 16:42:28 +00:00
for _ , mediaID := range form . MediaIDs {
2022-10-08 11:50:48 +00:00
attachment , err := p . db . GetAttachmentByID ( ctx , mediaID )
if err != nil {
2022-11-05 12:33:38 +00:00
if errors . Is ( err , db . ErrNoEntries ) {
err = fmt . Errorf ( "ProcessMediaIDs: media not found for media id %s" , mediaID )
return gtserror . NewErrorBadRequest ( err , err . Error ( ) )
}
err = fmt . Errorf ( "ProcessMediaIDs: db error for media id %s" , mediaID )
return gtserror . NewErrorInternalError ( err )
2021-06-13 16:42:28 +00:00
}
2022-10-08 11:50:48 +00:00
if attachment . AccountID != thisAccountID {
2022-11-05 12:33:38 +00:00
err = fmt . Errorf ( "ProcessMediaIDs: media with id %s does not belong to account %s" , mediaID , thisAccountID )
return gtserror . NewErrorBadRequest ( err , err . Error ( ) )
2021-06-13 16:42:28 +00:00
}
2022-10-08 11:50:48 +00:00
if attachment . StatusID != "" || attachment . ScheduledStatusID != "" {
2022-11-05 12:33:38 +00:00
err = fmt . Errorf ( "ProcessMediaIDs: media with id %s is already attached to a status" , mediaID )
return gtserror . NewErrorBadRequest ( err , err . Error ( ) )
}
minDescriptionChars := config . GetMediaDescriptionMinChars ( )
if descriptionLength := len ( [ ] rune ( attachment . Description ) ) ; descriptionLength < minDescriptionChars {
err = fmt . Errorf ( "ProcessMediaIDs: description too short! media description of at least %d chararacters is required but %d was provided for media with id %s" , minDescriptionChars , descriptionLength , mediaID )
return gtserror . NewErrorBadRequest ( err , err . Error ( ) )
2021-06-13 16:42:28 +00:00
}
2022-10-08 11:50:48 +00:00
attachments = append ( attachments , attachment )
attachmentIDs = append ( attachmentIDs , attachment . ID )
2021-06-13 16:42:28 +00:00
}
2022-10-08 11:50:48 +00:00
status . Attachments = attachments
status . AttachmentIDs = attachmentIDs
2021-06-13 16:42:28 +00:00
return nil
}
2021-08-25 13:34:33 +00:00
func ( p * processor ) ProcessLanguage ( ctx context . Context , form * apimodel . AdvancedStatusCreateForm , accountDefaultLanguage string , status * gtsmodel . Status ) error {
2021-06-13 16:42:28 +00:00
if form . Language != "" {
status . Language = form . Language
} else {
status . Language = accountDefaultLanguage
}
if status . Language == "" {
return errors . New ( "no language given either in status create form or account default" )
}
return nil
}
2021-08-25 13:34:33 +00:00
func ( p * processor ) ProcessContent ( ctx context . Context , form * apimodel . AdvancedStatusCreateForm , accountID string , status * gtsmodel . Status ) error {
2021-07-26 18:25:54 +00:00
// if there's nothing in the status at all we can just return early
2021-06-13 16:42:28 +00:00
if form . Status == "" {
status . Content = ""
return nil
}
2022-08-06 10:09:21 +00:00
// if format wasn't specified we should try to figure out what format this user prefers
2021-07-26 18:25:54 +00:00
if form . Format == "" {
2022-08-06 10:09:21 +00:00
acct , err := p . db . GetAccountByID ( ctx , accountID )
if err != nil {
return fmt . Errorf ( "error processing new content: couldn't retrieve account from db to check post format: %s" , err )
}
switch acct . StatusFormat {
case "plain" :
2023-01-02 12:10:50 +00:00
form . Format = apimodel . StatusFormatPlain
2022-08-06 10:09:21 +00:00
case "markdown" :
2023-01-02 12:10:50 +00:00
form . Format = apimodel . StatusFormatMarkdown
2022-08-06 10:09:21 +00:00
default :
2023-01-02 12:10:50 +00:00
form . Format = apimodel . StatusFormatDefault
2022-08-06 10:09:21 +00:00
}
2021-06-13 16:42:28 +00:00
}
2021-07-26 18:25:54 +00:00
// parse content out of the status depending on what format has been submitted
2023-02-03 10:58:58 +00:00
var f text . FormatFunc
2021-07-26 18:25:54 +00:00
switch form . Format {
case apimodel . StatusFormatPlain :
2023-02-03 10:58:58 +00:00
f = p . formatter . FromPlain
2021-07-26 18:25:54 +00:00
case apimodel . StatusFormatMarkdown :
2023-02-03 10:58:58 +00:00
f = p . formatter . FromMarkdown
2021-07-26 18:25:54 +00:00
default :
return fmt . Errorf ( "format %s not recognised as a valid status format" , form . Format )
2021-06-13 16:42:28 +00:00
}
2023-02-03 10:58:58 +00:00
formatted := f ( ctx , p . parseMention , accountID , status . ID , form . Status )
// add full populated gts {mentions, tags, emojis} to the status for passing them around conveniently
// add just their ids to the status for putting in the db
status . Mentions = formatted . Mentions
status . MentionIDs = make ( [ ] string , 0 , len ( formatted . Mentions ) )
for _ , gtsmention := range formatted . Mentions {
status . MentionIDs = append ( status . MentionIDs , gtsmention . ID )
}
status . Tags = formatted . Tags
status . TagIDs = make ( [ ] string , 0 , len ( formatted . Tags ) )
for _ , gtstag := range formatted . Tags {
status . TagIDs = append ( status . TagIDs , gtstag . ID )
}
status . Emojis = formatted . Emojis
status . EmojiIDs = make ( [ ] string , 0 , len ( formatted . Emojis ) )
for _ , gtsemoji := range formatted . Emojis {
status . EmojiIDs = append ( status . EmojiIDs , gtsemoji . ID )
}
spoilerformatted := p . formatter . FromPlainEmojiOnly ( ctx , p . parseMention , accountID , status . ID , form . SpoilerText )
for _ , gtsemoji := range spoilerformatted . Emojis {
status . Emojis = append ( status . Emojis , gtsemoji )
status . EmojiIDs = append ( status . EmojiIDs , gtsemoji . ID )
}
2021-06-13 16:42:28 +00:00
2023-02-03 10:58:58 +00:00
status . Content = formatted . HTML
2021-06-13 16:42:28 +00:00
return nil
}