2021-08-25 13:34:33 +00:00
/ *
GoToSocial
2021-12-20 17:42:19 +00:00
Copyright ( C ) 2021 - 2022 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"
2022-08-06 10:09:21 +00:00
"github.com/superseriousbusiness/gotosocial/internal/api/model"
2021-06-13 16:42:28 +00:00
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"
2022-07-19 08:47:55 +00:00
"github.com/superseriousbusiness/gotosocial/internal/log"
2021-06-13 16:42:28 +00:00
"github.com/superseriousbusiness/gotosocial/internal/util"
)
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 ) ProcessMentions ( ctx context . Context , form * apimodel . AdvancedStatusCreateForm , accountID string , status * gtsmodel . Status ) error {
2022-03-29 09:54:56 +00:00
mentionedAccountNames := util . DeriveMentionNamesFromText ( form . Status )
mentions := [ ] * gtsmodel . Mention { }
mentionIDs := [ ] string { }
for _ , mentionedAccountName := range mentionedAccountNames {
gtsMention , err := p . parseMention ( ctx , mentionedAccountName , accountID , status . ID )
2021-06-13 16:42:28 +00:00
if err != nil {
2022-07-19 08:47:55 +00:00
log . Errorf ( "ProcessMentions: error parsing mention %s from status: %s" , mentionedAccountName , err )
2022-03-29 09:54:56 +00:00
continue
2021-06-13 16:42:28 +00:00
}
2022-03-29 09:54:56 +00:00
if err := p . db . Put ( ctx , gtsMention ) ; err != nil {
2022-07-19 08:47:55 +00:00
log . Errorf ( "ProcessMentions: error putting mention in db: %s" , err )
2021-06-13 16:42:28 +00:00
}
2022-03-29 09:54:56 +00:00
mentions = append ( mentions , gtsMention )
mentionIDs = append ( mentionIDs , gtsMention . ID )
2021-06-13 16:42:28 +00:00
}
2022-03-29 09:54:56 +00:00
2021-06-13 16:42:28 +00:00
// add full populated gts menchies to the status for passing them around conveniently
2022-03-29 09:54:56 +00:00
status . Mentions = mentions
2021-06-13 16:42:28 +00:00
// add just the ids of the mentioned accounts to the status for putting in the db
2022-03-29 09:54:56 +00:00
status . MentionIDs = mentionIDs
2021-06-13 16:42:28 +00:00
return nil
}
2021-08-25 13:34:33 +00:00
func ( p * processor ) ProcessTags ( ctx context . Context , form * apimodel . AdvancedStatusCreateForm , accountID string , status * gtsmodel . Status ) error {
2021-06-13 16:42:28 +00:00
tags := [ ] string { }
2021-09-11 11:19:06 +00:00
gtsTags , err := p . db . TagStringsToTags ( ctx , util . DeriveHashtagsFromText ( form . Status ) , accountID )
2021-06-13 16:42:28 +00:00
if err != nil {
return fmt . Errorf ( "error generating hashtags from status: %s" , err )
}
for _ , tag := range gtsTags {
2022-02-07 11:04:31 +00:00
if err := p . db . Put ( ctx , tag ) ; err != nil {
2022-09-12 11:03:23 +00:00
if ! errors . Is ( err , db . ErrAlreadyExists ) {
2022-02-07 11:04:31 +00:00
return fmt . Errorf ( "error putting tags in db: %s" , err )
}
2021-06-13 16:42:28 +00:00
}
tags = append ( tags , tag . ID )
}
// add full populated gts tags to the status for passing them around conveniently
2021-08-20 10:26:56 +00:00
status . Tags = gtsTags
2021-06-13 16:42:28 +00:00
// add just the ids of the used tags to the status for putting in the db
2021-08-20 10:26:56 +00:00
status . TagIDs = tags
2021-06-13 16:42:28 +00:00
return nil
}
2021-08-25 13:34:33 +00:00
func ( p * processor ) ProcessEmojis ( ctx context . Context , form * apimodel . AdvancedStatusCreateForm , accountID string , status * gtsmodel . Status ) error {
2022-09-06 10:42:55 +00:00
// for each emoji shortcode in the text, check if it's an enabled
// emoji on this instance, and if so, add it to the status
2022-10-02 12:01:47 +00:00
emojiShortcodes := util . DeriveEmojisFromText ( form . SpoilerText + "\n\n" + form . Status )
2022-09-06 10:42:55 +00:00
status . Emojis = make ( [ ] * gtsmodel . Emoji , 0 , len ( emojiShortcodes ) )
status . EmojiIDs = make ( [ ] string , 0 , len ( emojiShortcodes ) )
for _ , shortcode := range emojiShortcodes {
emoji , err := p . db . GetEmojiByShortcodeDomain ( ctx , shortcode , "" )
if err != nil {
if err != db . ErrNoEntries {
log . Errorf ( "error getting local emoji with shortcode %s: %s" , shortcode , err )
}
continue
}
if * emoji . VisibleInPicker && ! * emoji . Disabled {
status . Emojis = append ( status . Emojis , emoji )
status . EmojiIDs = append ( status . EmojiIDs , emoji . ID )
}
2021-06-13 16:42:28 +00:00
}
2022-09-06 10:42:55 +00:00
2021-06-13 16:42:28 +00:00
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" :
form . Format = model . StatusFormatPlain
case "markdown" :
form . Format = model . StatusFormatMarkdown
default :
form . Format = model . StatusFormatDefault
}
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
2021-08-11 14:54:54 +00:00
var formatted string
2021-07-26 18:25:54 +00:00
switch form . Format {
case apimodel . StatusFormatPlain :
2022-05-26 09:37:13 +00:00
formatted = p . formatter . FromPlain ( ctx , form . Status , status . Mentions , status . Tags )
2021-07-26 18:25:54 +00:00
case apimodel . StatusFormatMarkdown :
2022-09-27 12:27:53 +00:00
formatted = p . formatter . FromMarkdown ( ctx , form . Status , status . Mentions , status . Tags , status . Emojis )
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
}
2021-08-11 14:54:54 +00:00
status . Content = formatted
2021-06-13 16:42:28 +00:00
return nil
}