diff options
Diffstat (limited to 'internal/apimodule/status/statuscreate.go')
-rw-r--r-- | internal/apimodule/status/statuscreate.go | 462 |
1 files changed, 0 insertions, 462 deletions
diff --git a/internal/apimodule/status/statuscreate.go b/internal/apimodule/status/statuscreate.go deleted file mode 100644 index 97354e767..000000000 --- a/internal/apimodule/status/statuscreate.go +++ /dev/null @@ -1,462 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021 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 status - -import ( - "errors" - "fmt" - "net/http" - "time" - - "github.com/gin-gonic/gin" - "github.com/google/uuid" - "github.com/superseriousbusiness/gotosocial/internal/config" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/distributor" - mastotypes "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel" - "github.com/superseriousbusiness/gotosocial/internal/oauth" - "github.com/superseriousbusiness/gotosocial/internal/util" -) - -type advancedStatusCreateForm struct { - mastotypes.StatusCreateRequest - advancedVisibilityFlagsForm -} - -type advancedVisibilityFlagsForm struct { - // The gotosocial visibility model - VisibilityAdvanced *gtsmodel.Visibility `form:"visibility_advanced"` - // This status will be federated beyond the local timeline(s) - Federated *bool `form:"federated"` - // This status can be boosted/reblogged - Boostable *bool `form:"boostable"` - // This status can be replied to - Replyable *bool `form:"replyable"` - // This status can be liked/faved - Likeable *bool `form:"likeable"` -} - -// StatusCreatePOSTHandler deals with the creation of new statuses -func (m *Module) StatusCreatePOSTHandler(c *gin.Context) { - l := m.log.WithField("func", "statusCreatePOSTHandler") - authed, err := oauth.MustAuth(c, true, true, true, true) // posting a status is serious business so we want *everything* - if err != nil { - l.Debugf("couldn't auth: %s", err) - c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) - return - } - - // First check this user/account is permitted to post new statuses. - // There's no point continuing otherwise. - if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { - l.Debugf("couldn't auth: %s", err) - c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"}) - return - } - - // extract the status create form from the request context - l.Tracef("parsing request form: %s", c.Request.Form) - form := &advancedStatusCreateForm{} - if err := c.ShouldBind(form); err != nil || form == nil { - l.Debugf("could not parse form from request: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"}) - return - } - - // Give the fields on the request form a first pass to make sure the request is superficially valid. - l.Tracef("validating form %+v", form) - if err := validateCreateStatus(form, m.config.StatusesConfig); err != nil { - l.Debugf("error validating form: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // At this point we know the account is permitted to post, and we know the request form - // is valid (at least according to the API specifications and the instance configuration). - // So now we can start digging a bit deeper into the form and building up the new status from it. - - // first we create a new status and add some basic info to it - uris := util.GenerateURIs(authed.Account.Username, m.config.Protocol, m.config.Host) - thisStatusID := uuid.NewString() - thisStatusURI := fmt.Sprintf("%s/%s", uris.StatusesURI, thisStatusID) - thisStatusURL := fmt.Sprintf("%s/%s", uris.StatusesURL, thisStatusID) - newStatus := >smodel.Status{ - ID: thisStatusID, - URI: thisStatusURI, - URL: thisStatusURL, - Content: util.HTMLFormat(form.Status), - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Local: true, - AccountID: authed.Account.ID, - ContentWarning: form.SpoilerText, - ActivityStreamsType: gtsmodel.ActivityStreamsNote, - Sensitive: form.Sensitive, - Language: form.Language, - CreatedWithApplicationID: authed.Application.ID, - Text: form.Status, - } - - // check if replyToID is ok - if err := m.parseReplyToID(form, authed.Account.ID, newStatus); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // check if mediaIDs are ok - if err := m.parseMediaIDs(form, authed.Account.ID, newStatus); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // check if visibility settings are ok - if err := parseVisibility(form, authed.Account.Privacy, newStatus); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // handle language settings - if err := parseLanguage(form, authed.Account.Language, newStatus); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // handle mentions - if err := m.parseMentions(form, authed.Account.ID, newStatus); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - if err := m.parseTags(form, authed.Account.ID, newStatus); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - if err := m.parseEmojis(form, authed.Account.ID, newStatus); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - /* - FROM THIS POINT ONWARDS WE ARE HAPPY WITH THE STATUS -- it is valid and we will try to create it - */ - - // put the new status in the database, generating an ID for it in the process - if err := m.db.Put(newStatus); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - // change the status ID of the media attachments to the new status - for _, a := range newStatus.GTSMediaAttachments { - a.StatusID = newStatus.ID - a.UpdatedAt = time.Now() - if err := m.db.UpdateByID(a.ID, a); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - } - - // pass to the distributor to take care of side effects asynchronously -- federation, mentions, updating metadata, etc, etc - m.distributor.FromClientAPI() <- distributor.FromClientAPI{ - APObjectType: gtsmodel.ActivityStreamsNote, - APActivityType: gtsmodel.ActivityStreamsCreate, - Activity: newStatus, - } - - // return the frontend representation of the new status to the submitter - mastoStatus, err := m.mastoConverter.StatusToMasto(newStatus, authed.Account, authed.Account, nil, newStatus.GTSReplyToAccount, nil) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - c.JSON(http.StatusOK, mastoStatus) -} - -func validateCreateStatus(form *advancedStatusCreateForm, config *config.StatusesConfig) error { - // validate that, structurally, we have a valid status/post - if form.Status == "" && form.MediaIDs == nil && form.Poll == nil { - return errors.New("no status, media, or poll provided") - } - - if form.MediaIDs != nil && form.Poll != nil { - return errors.New("can't post media + poll in same status") - } - - // validate status - if form.Status != "" { - if len(form.Status) > config.MaxChars { - return fmt.Errorf("status too long, %d characters provided but limit is %d", len(form.Status), config.MaxChars) - } - } - - // validate media attachments - if len(form.MediaIDs) > config.MaxMediaFiles { - return fmt.Errorf("too many media files attached to status, %d attached but limit is %d", len(form.MediaIDs), config.MaxMediaFiles) - } - - // validate poll - if form.Poll != nil { - if form.Poll.Options == nil { - return errors.New("poll with no options") - } - if len(form.Poll.Options) > config.PollMaxOptions { - return fmt.Errorf("too many poll options provided, %d provided but limit is %d", len(form.Poll.Options), config.PollMaxOptions) - } - for _, p := range form.Poll.Options { - if len(p) > config.PollOptionMaxChars { - return fmt.Errorf("poll option too long, %d characters provided but limit is %d", len(p), config.PollOptionMaxChars) - } - } - } - - // validate spoiler text/cw - if form.SpoilerText != "" { - if len(form.SpoilerText) > config.CWMaxChars { - return fmt.Errorf("content-warning/spoilertext too long, %d characters provided but limit is %d", len(form.SpoilerText), config.CWMaxChars) - } - } - - // validate post language - if form.Language != "" { - if err := util.ValidateLanguage(form.Language); err != nil { - return err - } - } - - return nil -} - -func parseVisibility(form *advancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error { - // by default all flags are set to true - gtsAdvancedVis := >smodel.VisibilityAdvanced{ - Federated: true, - Boostable: true, - Replyable: true, - Likeable: true, - } - - var gtsBasicVis gtsmodel.Visibility - // Advanced takes priority if it's set. - // If it's not set, take whatever masto visibility is set. - // If *that's* not set either, then just take the account default. - // If that's also not set, take the default for the whole instance. - if form.VisibilityAdvanced != nil { - gtsBasicVis = *form.VisibilityAdvanced - } else if form.Visibility != "" { - gtsBasicVis = util.ParseGTSVisFromMastoVis(form.Visibility) - } else if accountDefaultVis != "" { - gtsBasicVis = accountDefaultVis - } else { - gtsBasicVis = gtsmodel.VisibilityDefault - } - - switch gtsBasicVis { - 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 { - gtsAdvancedVis.Federated = *form.Federated - } - - if form.Boostable != nil { - gtsAdvancedVis.Boostable = *form.Boostable - } - - if form.Replyable != nil { - gtsAdvancedVis.Replyable = *form.Replyable - } - - if form.Likeable != nil { - gtsAdvancedVis.Likeable = *form.Likeable - } - - 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 - gtsAdvancedVis.Boostable = false - - if form.Federated != nil { - gtsAdvancedVis.Federated = *form.Federated - } - - if form.Replyable != nil { - gtsAdvancedVis.Replyable = *form.Replyable - } - - if form.Likeable != nil { - gtsAdvancedVis.Likeable = *form.Likeable - } - - case gtsmodel.VisibilityDirect: - // direct is pretty easy: there's only one possible setting so return it - gtsAdvancedVis.Federated = true - gtsAdvancedVis.Boostable = false - gtsAdvancedVis.Federated = true - gtsAdvancedVis.Likeable = true - } - - status.Visibility = gtsBasicVis - status.VisibilityAdvanced = gtsAdvancedVis - return nil -} - -func (m *Module) parseReplyToID(form *advancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error { - 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 := >smodel.Status{} - repliedAccount := >smodel.Account{} - // check replied status exists + is replyable - if err := m.db.GetByID(form.InReplyToID, repliedStatus); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { - return fmt.Errorf("status with id %s not replyable because it doesn't exist", form.InReplyToID) - } - return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err) - } - - if !repliedStatus.VisibilityAdvanced.Replyable { - return fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID) - } - - // check replied account is known to us - if err := m.db.GetByID(repliedStatus.AccountID, repliedAccount); err != nil { - if _, ok := err.(db.ErrNoEntries); ok { - return fmt.Errorf("status with id %s not replyable because account id %s is not known", form.InReplyToID, repliedStatus.AccountID) - } - return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err) - } - // check if a block exists - if blocked, err := m.db.Blocked(thisAccountID, repliedAccount.ID); err != nil { - if _, ok := err.(db.ErrNoEntries); !ok { - return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err) - } - } else if blocked { - return fmt.Errorf("status with id %s not replyable", form.InReplyToID) - } - status.InReplyToID = repliedStatus.ID - status.InReplyToAccountID = repliedAccount.ID - - return nil -} - -func (m *Module) parseMediaIDs(form *advancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error { - if form.MediaIDs == nil { - return nil - } - - gtsMediaAttachments := []*gtsmodel.MediaAttachment{} - attachments := []string{} - for _, mediaID := range form.MediaIDs { - // check these attachments exist - a := >smodel.MediaAttachment{} - if err := m.db.GetByID(mediaID, a); err != nil { - return fmt.Errorf("invalid media type or media not found for media id %s", mediaID) - } - // check they belong to the requesting account id - if a.AccountID != thisAccountID { - return fmt.Errorf("media with id %s does not belong to account %s", mediaID, thisAccountID) - } - // check they're not already used in a status - if a.StatusID != "" || a.ScheduledStatusID != "" { - return fmt.Errorf("media with id %s is already attached to a status", mediaID) - } - gtsMediaAttachments = append(gtsMediaAttachments, a) - attachments = append(attachments, a.ID) - } - status.GTSMediaAttachments = gtsMediaAttachments - status.Attachments = attachments - return nil -} - -func parseLanguage(form *advancedStatusCreateForm, accountDefaultLanguage string, status *gtsmodel.Status) error { - 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 -} - -func (m *Module) parseMentions(form *advancedStatusCreateForm, accountID string, status *gtsmodel.Status) error { - menchies := []string{} - gtsMenchies, err := m.db.MentionStringsToMentions(util.DeriveMentions(form.Status), accountID, status.ID) - if err != nil { - return fmt.Errorf("error generating mentions from status: %s", err) - } - for _, menchie := range gtsMenchies { - if err := m.db.Put(menchie); err != nil { - return fmt.Errorf("error putting mentions in db: %s", err) - } - menchies = append(menchies, menchie.TargetAccountID) - } - // add full populated gts menchies to the status for passing them around conveniently - status.GTSMentions = gtsMenchies - // add just the ids of the mentioned accounts to the status for putting in the db - status.Mentions = menchies - return nil -} - -func (m *Module) parseTags(form *advancedStatusCreateForm, accountID string, status *gtsmodel.Status) error { - tags := []string{} - gtsTags, err := m.db.TagStringsToTags(util.DeriveHashtags(form.Status), accountID, status.ID) - if err != nil { - return fmt.Errorf("error generating hashtags from status: %s", err) - } - for _, tag := range gtsTags { - if err := m.db.Upsert(tag, "name"); err != nil { - return fmt.Errorf("error putting tags in db: %s", err) - } - tags = append(tags, tag.ID) - } - // add full populated gts tags to the status for passing them around conveniently - status.GTSTags = gtsTags - // add just the ids of the used tags to the status for putting in the db - status.Tags = tags - return nil -} - -func (m *Module) parseEmojis(form *advancedStatusCreateForm, accountID string, status *gtsmodel.Status) error { - emojis := []string{} - gtsEmojis, err := m.db.EmojiStringsToEmojis(util.DeriveEmojis(form.Status), accountID, status.ID) - if err != nil { - return fmt.Errorf("error generating emojis from status: %s", err) - } - for _, e := range gtsEmojis { - emojis = append(emojis, e.ID) - } - // add full populated gts emojis to the status for passing them around conveniently - status.GTSEmojis = gtsEmojis - // add just the ids of the used emojis to the status for putting in the db - status.Emojis = emojis - return nil -} |