summaryrefslogtreecommitdiff
path: root/internal/api
diff options
context:
space:
mode:
authorLibravatar kim <89579420+NyaaaWhatsUpDoc@users.noreply.github.com>2024-12-23 17:54:44 +0000
committerLibravatar GitHub <noreply@github.com>2024-12-23 17:54:44 +0000
commitfe8d5f23072c40a407723904eb5c54234879d58a (patch)
treedf80063f3238997de7144932d2d713321164ac1b /internal/api
parent[chore] Stub /api/v1/announcements implementation (#3630) (diff)
downloadgotosocial-fe8d5f23072c40a407723904eb5c54234879d58a.tar.xz
[feature] add support for clients editing statuses and fetching status revision history (#3628)
* start adding client support for making status edits and viewing history * modify 'freshest' freshness window to be 5s, add typeutils test for status -> api edits * only populate the status edits when specifically requested * start adding some simple processor status edit tests * add test editing status but adding a poll * test edits appropriately adding poll expiry handlers * finish adding status edit tests * store both new and old revision emojis in status * add code comment * ensure the requester's account is populated before status edits * add code comments for status edit tests * update status edit form swagger comments * remove unused function * fix status source test * add more code comments, move media description check back to media process in status create * fix tests, add necessary form struct tag
Diffstat (limited to 'internal/api')
-rw-r--r--internal/api/client/statuses/status.go3
-rw-r--r--internal/api/client/statuses/statuscreate.go156
-rw-r--r--internal/api/client/statuses/statusdelete.go2
-rw-r--r--internal/api/client/statuses/statusedit.go249
-rw-r--r--internal/api/client/statuses/statusedit_test.go32
-rw-r--r--internal/api/client/statuses/statussource_test.go2
-rw-r--r--internal/api/model/attachment.go25
-rw-r--r--internal/api/model/status.go70
-rw-r--r--internal/api/util/parseform.go42
9 files changed, 457 insertions, 124 deletions
diff --git a/internal/api/client/statuses/status.go b/internal/api/client/statuses/status.go
index 33af9c456..88b34cbf5 100644
--- a/internal/api/client/statuses/status.go
+++ b/internal/api/client/statuses/status.go
@@ -83,9 +83,10 @@ func New(processor *processing.Processor) *Module {
}
func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) {
- // create / get / delete status
+ // create / get / edit / delete status
attachHandler(http.MethodPost, BasePath, m.StatusCreatePOSTHandler)
attachHandler(http.MethodGet, BasePathWithID, m.StatusGETHandler)
+ attachHandler(http.MethodPut, BasePathWithID, m.StatusEditPUTHandler)
attachHandler(http.MethodDelete, BasePathWithID, m.StatusDELETEHandler)
// fave stuff
diff --git a/internal/api/client/statuses/statuscreate.go b/internal/api/client/statuses/statuscreate.go
index 8198d5358..c83cdbad7 100644
--- a/internal/api/client/statuses/statuscreate.go
+++ b/internal/api/client/statuses/statuscreate.go
@@ -27,11 +27,9 @@ import (
"github.com/go-playground/form/v4"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
- "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/util"
- "github.com/superseriousbusiness/gotosocial/internal/validate"
)
// StatusCreatePOSTHandler swagger:operation POST /api/v1/statuses statusCreate
@@ -272,9 +270,9 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {
return
}
- form, err := parseStatusCreateForm(c)
- if err != nil {
- apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ form, errWithCode := parseStatusCreateForm(c)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
}
@@ -287,11 +285,6 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {
// }
// form.Status += "\n\nsent from " + user + "'s iphone\n"
- if errWithCode := validateStatusCreateForm(form); errWithCode != nil {
- apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
- return
- }
-
apiStatus, errWithCode := m.processor.Status().Create(
c.Request.Context(),
authed.Account,
@@ -303,7 +296,7 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {
return
}
- c.JSON(http.StatusOK, apiStatus)
+ apiutil.JSON(c, http.StatusOK, apiStatus)
}
// intPolicyFormBinding satisfies gin's binding.Binding interface.
@@ -328,108 +321,69 @@ func (intPolicyFormBinding) Bind(req *http.Request, obj any) error {
return decoder.Decode(obj, req.Form)
}
-func parseStatusCreateForm(c *gin.Context) (*apimodel.StatusCreateRequest, error) {
+func parseStatusCreateForm(c *gin.Context) (*apimodel.StatusCreateRequest, gtserror.WithCode) {
form := new(apimodel.StatusCreateRequest)
switch ct := c.ContentType(); ct {
case binding.MIMEJSON:
// Just bind with default json binding.
if err := c.ShouldBindWith(form, binding.JSON); err != nil {
- return nil, err
+ return nil, gtserror.NewErrorBadRequest(
+ err,
+ err.Error(),
+ )
}
case binding.MIMEPOSTForm:
// Bind with default form binding first.
if err := c.ShouldBindWith(form, binding.FormPost); err != nil {
- return nil, err
+ return nil, gtserror.NewErrorBadRequest(
+ err,
+ err.Error(),
+ )
}
// Now do custom binding.
intReqForm := new(apimodel.StatusInteractionPolicyForm)
if err := c.ShouldBindWith(intReqForm, intPolicyFormBinding{}); err != nil {
- return nil, err
+ return nil, gtserror.NewErrorBadRequest(
+ err,
+ err.Error(),
+ )
}
+
form.InteractionPolicy = intReqForm.InteractionPolicy
case binding.MIMEMultipartPOSTForm:
// Bind with default form binding first.
if err := c.ShouldBindWith(form, binding.FormMultipart); err != nil {
- return nil, err
+ return nil, gtserror.NewErrorBadRequest(
+ err,
+ err.Error(),
+ )
}
// Now do custom binding.
intReqForm := new(apimodel.StatusInteractionPolicyForm)
if err := c.ShouldBindWith(intReqForm, intPolicyFormBinding{}); err != nil {
- return nil, err
+ return nil, gtserror.NewErrorBadRequest(
+ err,
+ err.Error(),
+ )
}
+
form.InteractionPolicy = intReqForm.InteractionPolicy
default:
- err := fmt.Errorf(
- "content-type %s not supported for this endpoint; supported content-types are %s, %s, %s",
- ct, binding.MIMEJSON, binding.MIMEPOSTForm, binding.MIMEMultipartPOSTForm,
- )
- return nil, err
- }
-
- return form, nil
-}
-
-// validateStatusCreateForm checks the form for disallowed
-// combinations of attachments, overlength inputs, etc.
-//
-// Side effect: normalizes the post's language tag.
-func validateStatusCreateForm(form *apimodel.StatusCreateRequest) gtserror.WithCode {
- var (
- chars = len([]rune(form.Status)) + len([]rune(form.SpoilerText))
- maxChars = config.GetStatusesMaxChars()
- mediaFiles = len(form.MediaIDs)
- maxMediaFiles = config.GetStatusesMediaMaxFiles()
- hasMedia = mediaFiles != 0
- hasPoll = form.Poll != nil
- )
-
- if chars == 0 && !hasMedia && !hasPoll {
- // Status must contain *some* kind of content.
- const text = "no status content, content warning, media, or poll provided"
- return gtserror.NewErrorBadRequest(errors.New(text), text)
- }
-
- if chars > maxChars {
- text := fmt.Sprintf(
- "status too long, %d characters provided (including content warning) but limit is %d",
- chars, maxChars,
- )
- return gtserror.NewErrorBadRequest(errors.New(text), text)
- }
-
- if mediaFiles > maxMediaFiles {
- text := fmt.Sprintf(
- "too many media files attached to status, %d attached but limit is %d",
- mediaFiles, maxMediaFiles,
- )
- return gtserror.NewErrorBadRequest(errors.New(text), text)
- }
-
- if form.Poll != nil {
- if errWithCode := validateStatusPoll(form); errWithCode != nil {
- return errWithCode
- }
+ text := fmt.Sprintf("content-type %s not supported for this endpoint; supported content-types are %s, %s, %s",
+ ct, binding.MIMEJSON, binding.MIMEPOSTForm, binding.MIMEMultipartPOSTForm)
+ return nil, gtserror.NewErrorNotAcceptable(errors.New(text), text)
}
+ // Check not scheduled status.
if form.ScheduledAt != "" {
const text = "scheduled_at is not yet implemented"
- return gtserror.NewErrorNotImplemented(errors.New(text), text)
- }
-
- // Validate + normalize
- // language tag if provided.
- if form.Language != "" {
- lang, err := validate.Language(form.Language)
- if err != nil {
- return gtserror.NewErrorBadRequest(err, err.Error())
- }
- form.Language = lang
+ return nil, gtserror.NewErrorNotImplemented(errors.New(text), text)
}
// Check if the deprecated "federated" field was
@@ -438,42 +392,9 @@ func validateStatusCreateForm(form *apimodel.StatusCreateRequest) gtserror.WithC
form.LocalOnly = util.Ptr(!*form.Federated) // nolint:staticcheck
}
- return nil
-}
-
-func validateStatusPoll(form *apimodel.StatusCreateRequest) gtserror.WithCode {
- var (
- maxPollOptions = config.GetStatusesPollMaxOptions()
- pollOptions = len(form.Poll.Options)
- maxPollOptionChars = config.GetStatusesPollOptionMaxChars()
- )
+ // Normalize poll expiry time if a poll was given.
+ if form.Poll != nil && form.Poll.ExpiresInI != nil {
- if pollOptions == 0 {
- const text = "poll with no options"
- return gtserror.NewErrorBadRequest(errors.New(text), text)
- }
-
- if pollOptions > maxPollOptions {
- text := fmt.Sprintf(
- "too many poll options provided, %d provided but limit is %d",
- pollOptions, maxPollOptions,
- )
- return gtserror.NewErrorBadRequest(errors.New(text), text)
- }
-
- for _, option := range form.Poll.Options {
- optionChars := len([]rune(option))
- if optionChars > maxPollOptionChars {
- text := fmt.Sprintf(
- "poll option too long, %d characters provided but limit is %d",
- optionChars, maxPollOptionChars,
- )
- return gtserror.NewErrorBadRequest(errors.New(text), text)
- }
- }
-
- // Normalize poll expiry if necessary.
- if form.Poll.ExpiresInI != nil {
// If we parsed this as JSON, expires_in
// may be either a float64 or a string.
expiresIn, err := apiutil.ParseDuration(
@@ -481,13 +402,10 @@ func validateStatusPoll(form *apimodel.StatusCreateRequest) gtserror.WithCode {
"expires_in",
)
if err != nil {
- return gtserror.NewErrorBadRequest(err, err.Error())
- }
-
- if expiresIn != nil {
- form.Poll.ExpiresIn = *expiresIn
+ return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
+ form.Poll.ExpiresIn = util.PtrOrZero(expiresIn)
}
- return nil
+ return form, nil
}
diff --git a/internal/api/client/statuses/statusdelete.go b/internal/api/client/statuses/statusdelete.go
index 7ee240dff..fa62d6893 100644
--- a/internal/api/client/statuses/statusdelete.go
+++ b/internal/api/client/statuses/statusdelete.go
@@ -95,5 +95,5 @@ func (m *Module) StatusDELETEHandler(c *gin.Context) {
return
}
- c.JSON(http.StatusOK, apiStatus)
+ apiutil.JSON(c, http.StatusOK, apiStatus)
}
diff --git a/internal/api/client/statuses/statusedit.go b/internal/api/client/statuses/statusedit.go
new file mode 100644
index 000000000..dfd7d651e
--- /dev/null
+++ b/internal/api/client/statuses/statusedit.go
@@ -0,0 +1,249 @@
+// 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/>.
+
+package statuses
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ "github.com/gin-gonic/gin/binding"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+// StatusEditPUTHandler swagger:operation PUT /api/v1/statuses statusEdit
+//
+// Edit an existing status using the given form field parameters.
+//
+// The parameters can also be given in the body of the request, as JSON, if the content-type is set to 'application/json'.
+//
+// ---
+// tags:
+// - statuses
+//
+// consumes:
+// - application/json
+// - application/x-www-form-urlencoded
+//
+// parameters:
+// -
+// name: status
+// x-go-name: Status
+// description: |-
+// Text content of the status.
+// If media_ids is provided, this becomes optional.
+// Attaching a poll is optional while status is provided.
+// type: string
+// in: formData
+// -
+// name: media_ids
+// x-go-name: MediaIDs
+// description: |-
+// Array of Attachment ids to be attached as media.
+// If provided, status becomes optional, and poll cannot be used.
+//
+// If the status is being submitted as a form, the key is 'media_ids[]',
+// but if it's json or xml, the key is 'media_ids'.
+// type: array
+// items:
+// type: string
+// in: formData
+// -
+// name: poll[options][]
+// x-go-name: PollOptions
+// description: |-
+// Array of possible poll answers.
+// If provided, media_ids cannot be used, and poll[expires_in] must be provided.
+// type: array
+// items:
+// type: string
+// in: formData
+// -
+// name: poll[expires_in]
+// x-go-name: PollExpiresIn
+// description: |-
+// Duration the poll should be open, in seconds.
+// If provided, media_ids cannot be used, and poll[options] must be provided.
+// type: integer
+// format: int64
+// in: formData
+// -
+// name: poll[multiple]
+// x-go-name: PollMultiple
+// description: Allow multiple choices on this poll.
+// type: boolean
+// default: false
+// in: formData
+// -
+// name: poll[hide_totals]
+// x-go-name: PollHideTotals
+// description: Hide vote counts until the poll ends.
+// type: boolean
+// default: true
+// in: formData
+// -
+// name: sensitive
+// x-go-name: Sensitive
+// description: Status and attached media should be marked as sensitive.
+// type: boolean
+// in: formData
+// -
+// name: spoiler_text
+// x-go-name: SpoilerText
+// description: |-
+// Text to be shown as a warning or subject before the actual content.
+// Statuses are generally collapsed behind this field.
+// type: string
+// in: formData
+// -
+// name: language
+// x-go-name: Language
+// description: ISO 639 language code for this status.
+// type: string
+// in: formData
+// -
+// name: content_type
+// x-go-name: ContentType
+// description: Content type to use when parsing this status.
+// type: string
+// enum:
+// - text/plain
+// - text/markdown
+// in: formData
+//
+// produces:
+// - application/json
+//
+// security:
+// - OAuth2 Bearer:
+// - write:statuses
+//
+// responses:
+// '200':
+// description: "The latest status revision."
+// schema:
+// "$ref": "#/definitions/status"
+// '400':
+// description: bad request
+// '401':
+// description: unauthorized
+// '403':
+// description: forbidden
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
+func (m *Module) StatusEditPUTHandler(c *gin.Context) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ form, errWithCode := parseStatusEditForm(c)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ apiStatus, errWithCode := m.processor.Status().Edit(
+ c.Request.Context(),
+ authed.Account,
+ c.Param(IDKey),
+ form,
+ )
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ apiutil.JSON(c, http.StatusOK, apiStatus)
+}
+
+func parseStatusEditForm(c *gin.Context) (*apimodel.StatusEditRequest, gtserror.WithCode) {
+ form := new(apimodel.StatusEditRequest)
+
+ switch ct := c.ContentType(); ct {
+ case binding.MIMEJSON:
+ // Just bind with default json binding.
+ if err := c.ShouldBindWith(form, binding.JSON); err != nil {
+ return nil, gtserror.NewErrorBadRequest(
+ err,
+ err.Error(),
+ )
+ }
+
+ case binding.MIMEPOSTForm:
+ // Bind with default form binding first.
+ if err := c.ShouldBindWith(form, binding.FormPost); err != nil {
+ return nil, gtserror.NewErrorBadRequest(
+ err,
+ err.Error(),
+ )
+ }
+
+ case binding.MIMEMultipartPOSTForm:
+ // Bind with default form binding first.
+ if err := c.ShouldBindWith(form, binding.FormMultipart); err != nil {
+ return nil, gtserror.NewErrorBadRequest(
+ err,
+ err.Error(),
+ )
+ }
+
+ default:
+ text := fmt.Sprintf("content-type %s not supported for this endpoint; supported content-types are %s, %s, %s",
+ ct, binding.MIMEJSON, binding.MIMEPOSTForm, binding.MIMEMultipartPOSTForm)
+ return nil, gtserror.NewErrorNotAcceptable(errors.New(text), text)
+ }
+
+ // Normalize poll expiry time if a poll was given.
+ if form.Poll != nil && form.Poll.ExpiresInI != nil {
+
+ // If we parsed this as JSON, expires_in
+ // may be either a float64 or a string.
+ expiresIn, err := apiutil.ParseDuration(
+ form.Poll.ExpiresInI,
+ "expires_in",
+ )
+ if err != nil {
+ return nil, gtserror.NewErrorBadRequest(err, err.Error())
+ }
+ form.Poll.ExpiresIn = util.PtrOrZero(expiresIn)
+ }
+
+ return form, nil
+
+}
diff --git a/internal/api/client/statuses/statusedit_test.go b/internal/api/client/statuses/statusedit_test.go
new file mode 100644
index 000000000..43b283d6d
--- /dev/null
+++ b/internal/api/client/statuses/statusedit_test.go
@@ -0,0 +1,32 @@
+// 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/>.
+
+package statuses_test
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+)
+
+type StatusEditTestSuite struct {
+ StatusStandardTestSuite
+}
+
+func TestStatusEditTestSuite(t *testing.T) {
+ suite.Run(t, new(StatusEditTestSuite))
+}
diff --git a/internal/api/client/statuses/statussource_test.go b/internal/api/client/statuses/statussource_test.go
index 28b1e6852..797a462ed 100644
--- a/internal/api/client/statuses/statussource_test.go
+++ b/internal/api/client/statuses/statussource_test.go
@@ -91,7 +91,7 @@ func (suite *StatusSourceTestSuite) TestGetSource() {
suite.Equal(`{
"id": "01F8MHAMCHF6Y650WCRSCP4WMY",
- "text": "**STATUS EDITS ARE NOT CURRENTLY SUPPORTED IN GOTOSOCIAL (coming in 2024)**\nYou can review the original text of your status below, but you will not be able to submit this edit.\n\n---\n\nhello everyone!",
+ "text": "hello everyone!",
"spoiler_text": "introduction post"
}`, dst.String())
}
diff --git a/internal/api/model/attachment.go b/internal/api/model/attachment.go
index f037a09aa..1d910343c 100644
--- a/internal/api/model/attachment.go
+++ b/internal/api/model/attachment.go
@@ -23,12 +23,15 @@ import "mime/multipart"
//
// swagger: ignore
type AttachmentRequest struct {
+
// Media file.
File *multipart.FileHeader `form:"file" binding:"required"`
+
// Description of the media file. Optional.
// This will be used as alt-text for users of screenreaders etc.
// example: This is an image of some kittens, they are very cute and fluffy.
Description string `form:"description"`
+
// Focus of the media file. Optional.
// If present, it should be in the form of two comma-separated floats between -1 and 1.
// example: -0.5,0.565
@@ -39,16 +42,38 @@ type AttachmentRequest struct {
//
// swagger:ignore
type AttachmentUpdateRequest struct {
+
// Description of the media file.
// This will be used as alt-text for users of screenreaders etc.
// allowEmptyValue: true
Description *string `form:"description" json:"description" xml:"description"`
+
// Focus of the media file.
// If present, it should be in the form of two comma-separated floats between -1 and 1.
// allowEmptyValue: true
Focus *string `form:"focus" json:"focus" xml:"focus"`
}
+// AttachmentAttributesRequest models an edit request for attachment attributes.
+//
+// swagger:ignore
+type AttachmentAttributesRequest struct {
+
+ // The ID of the attachment.
+ // example: 01FC31DZT1AYWDZ8XTCRWRBYRK
+ ID string `form:"id" json:"id"`
+
+ // Description of the media file.
+ // This will be used as alt-text for users of screenreaders etc.
+ // allowEmptyValue: true
+ Description string `form:"description" json:"description"`
+
+ // Focus of the media file.
+ // If present, it should be in the form of two comma-separated floats between -1 and 1.
+ // allowEmptyValue: true
+ Focus string `form:"focus" json:"focus"`
+}
+
// Attachment models a media attachment.
//
// swagger:model attachment
diff --git a/internal/api/model/status.go b/internal/api/model/status.go
index 724134b77..ea9fbaa35 100644
--- a/internal/api/model/status.go
+++ b/internal/api/model/status.go
@@ -197,36 +197,50 @@ type StatusReblogged struct {
//
// swagger:ignore
type StatusCreateRequest struct {
+
// Text content of the status.
// If media_ids is provided, this becomes optional.
// Attaching a poll is optional while status is provided.
Status string `form:"status" json:"status"`
+
// Array of Attachment ids to be attached as media.
// If provided, status becomes optional, and poll cannot be used.
MediaIDs []string `form:"media_ids[]" json:"media_ids"`
+
// Poll to include with this status.
Poll *PollRequest `form:"poll" json:"poll"`
+
// ID of the status being replied to, if status is a reply.
InReplyToID string `form:"in_reply_to_id" json:"in_reply_to_id"`
+
// Status and attached media should be marked as sensitive.
Sensitive bool `form:"sensitive" json:"sensitive"`
+
// Text to be shown as a warning or subject before the actual content.
// Statuses are generally collapsed behind this field.
SpoilerText string `form:"spoiler_text" json:"spoiler_text"`
+
// Visibility of the posted status.
Visibility Visibility `form:"visibility" json:"visibility"`
- // Set to "true" if this status should not be federated, ie. it should be a "local only" status.
+
+ // Set to "true" if this status should not be
+ // federated,ie. it should be a "local only" status.
LocalOnly *bool `form:"local_only" json:"local_only"`
+
// Deprecated: Only used if LocalOnly is not set.
Federated *bool `form:"federated" json:"federated"`
+
// ISO 8601 Datetime at which to schedule a status.
// Providing this parameter will cause ScheduledStatus to be returned instead of Status.
// Must be at least 5 minutes in the future.
ScheduledAt string `form:"scheduled_at" json:"scheduled_at"`
+
// ISO 639 language code for this status.
Language string `form:"language" json:"language"`
+
// Content type to use when parsing this status.
ContentType StatusContentType `form:"content_type" json:"content_type"`
+
// Interaction policy to use for this status.
InteractionPolicy *InteractionPolicy `form:"-" json:"interaction_policy"`
}
@@ -236,6 +250,7 @@ type StatusCreateRequest struct {
//
// swagger:ignore
type StatusInteractionPolicyForm struct {
+
// Interaction policy to use for this status.
InteractionPolicy *InteractionPolicy `form:"interaction_policy" json:"-"`
}
@@ -250,13 +265,18 @@ const (
// VisibilityNone is visible to nobody. This is only used for the visibility of web statuses.
VisibilityNone Visibility = "none"
// VisibilityPublic is visible to everyone, and will be available via the web even for nonauthenticated users.
+
VisibilityPublic Visibility = "public"
+
// VisibilityUnlisted is visible to everyone, but only on home timelines, lists, etc.
VisibilityUnlisted Visibility = "unlisted"
+
// VisibilityPrivate is visible only to followers of the account that posted the status.
VisibilityPrivate Visibility = "private"
+
// VisibilityMutualsOnly is visible only to mutual followers of the account that posted the status.
VisibilityMutualsOnly Visibility = "mutuals_only"
+
// VisibilityDirect is visible only to accounts tagged in the status. It is equivalent to a direct message.
VisibilityDirect Visibility = "direct"
)
@@ -268,7 +288,8 @@ const (
// swagger:type string
type StatusContentType string
-// Content type to use when parsing submitted status into an html-formatted status
+// Content type to use when parsing submitted
+// status into an html-formatted status.
const (
StatusContentTypePlain StatusContentType = "text/plain"
StatusContentTypeMarkdown StatusContentType = "text/markdown"
@@ -280,11 +301,14 @@ const (
//
// swagger:model statusSource
type StatusSource struct {
+
// ID of the status.
// example: 01FBVD42CQ3ZEEVMW180SBX03B
ID string `json:"id"`
+
// Plain-text source of a status.
Text string `json:"text"`
+
// Plain-text version of spoiler text.
SpoilerText string `json:"spoiler_text"`
}
@@ -294,27 +318,69 @@ type StatusSource struct {
//
// swagger:model statusEdit
type StatusEdit struct {
+
// The content of this status at this revision.
// Should be HTML, but might also be plaintext in some cases.
// example: <p>Hey this is a status!</p>
Content string `json:"content"`
+
// Subject, summary, or content warning for the status at this revision.
// example: warning nsfw
SpoilerText string `json:"spoiler_text"`
+
// Status marked sensitive at this revision.
// example: false
Sensitive bool `json:"sensitive"`
+
// The date when this revision was created (ISO 8601 Datetime).
// example: 2021-07-30T09:20:25+00:00
CreatedAt string `json:"created_at"`
+
// The account that authored this status.
Account *Account `json:"account"`
+
// The poll attached to the status at this revision.
// Note that edits changing the poll options will be collapsed together into one edit, since this action resets the poll.
// nullable: true
Poll *Poll `json:"poll"`
+
// Media that is attached to this status.
MediaAttachments []*Attachment `json:"media_attachments"`
+
// Custom emoji to be used when rendering status content.
Emojis []Emoji `json:"emojis"`
}
+
+// StatusEditRequest models status edit parameters.
+//
+// swagger:ignore
+type StatusEditRequest struct {
+
+ // Text content of the status.
+ // If media_ids is provided, this becomes optional.
+ // Attaching a poll is optional while status is provided.
+ Status string `form:"status" json:"status"`
+
+ // Text to be shown as a warning or subject before the actual content.
+ // Statuses are generally collapsed behind this field.
+ SpoilerText string `form:"spoiler_text" json:"spoiler_text"`
+
+ // Content type to use when parsing this status.
+ ContentType StatusContentType `form:"content_type" json:"content_type"`
+
+ // Status and attached media should be marked as sensitive.
+ Sensitive bool `form:"sensitive" json:"sensitive"`
+
+ // ISO 639 language code for this status.
+ Language string `form:"language" json:"language"`
+
+ // Array of Attachment ids to be attached as media.
+ // If provided, status becomes optional, and poll cannot be used.
+ MediaIDs []string `form:"media_ids[]" json:"media_ids"`
+
+ // Array of Attachment attributes to be updated in attached media.
+ MediaAttributes []AttachmentAttributesRequest `form:"media_attributes[]" json:"media_attributes"`
+
+ // Poll to include with this status.
+ Poll *PollRequest `form:"poll" json:"poll"`
+}
diff --git a/internal/api/util/parseform.go b/internal/api/util/parseform.go
index 3eab065f2..8bb10012c 100644
--- a/internal/api/util/parseform.go
+++ b/internal/api/util/parseform.go
@@ -18,13 +18,55 @@
package util
import (
+ "errors"
"fmt"
"strconv"
+ "strings"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
+// ParseFocus parses a media attachment focus parameters from incoming API string.
+func ParseFocus(focus string) (focusx, focusy float32, errWithCode gtserror.WithCode) {
+ if focus == "" {
+ return
+ }
+ spl := strings.Split(focus, ",")
+ if len(spl) != 2 {
+ const text = "missing comma separator"
+ errWithCode = gtserror.NewErrorBadRequest(
+ errors.New(text),
+ text,
+ )
+ return
+ }
+ xStr := spl[0]
+ yStr := spl[1]
+ fx, err := strconv.ParseFloat(xStr, 32)
+ if err != nil || fx > 1 || fx < -1 {
+ text := fmt.Sprintf("invalid x focus: %s", xStr)
+ errWithCode = gtserror.NewErrorBadRequest(
+ errors.New(text),
+ text,
+ )
+ return
+ }
+ fy, err := strconv.ParseFloat(yStr, 32)
+ if err != nil || fy > 1 || fy < -1 {
+ text := fmt.Sprintf("invalid y focus: %s", xStr)
+ errWithCode = gtserror.NewErrorBadRequest(
+ errors.New(text),
+ text,
+ )
+ return
+ }
+ focusx = float32(fx)
+ focusy = float32(fy)
+ return
+}
+
// ParseDuration parses the given raw interface belonging
// the given fieldName as an integer duration.
func ParseDuration(rawI any, fieldName string) (*int, error) {