mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2024-11-10 06:54:16 +00:00
[feature] Allow users to submit interaction_policy
on new statuses (#3314)
* [feature] Parse `interaction_policy` on status submission * beep boop * swagger? i barely know er
This commit is contained in:
parent
f819229988
commit
c378ad2bb3
6 changed files with 1342 additions and 413 deletions
|
@ -8826,11 +8826,27 @@ paths:
|
|||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
- application/xml
|
||||
- application/x-www-form-urlencoded
|
||||
description: |-
|
||||
The parameters can also be given in the body of the request, as JSON, if the content-type is set to 'application/json'.
|
||||
The parameters can also be given in the body of the request, as XML, if the content-type is set to 'application/xml'.
|
||||
|
||||
The 'interaction_policy' field can be used to set an interaction policy for this status.
|
||||
|
||||
If submitting using form data, use the following pattern to set an interaction policy:
|
||||
|
||||
`interaction_policy[INTERACTION_TYPE][CONDITION][INDEX]=Value`
|
||||
|
||||
For example: `interaction_policy[can_reply][always][0]=author`
|
||||
|
||||
Using `curl` this might look something like:
|
||||
|
||||
`curl -F 'interaction_policy[can_reply][always][0]=author' -F 'interaction_policy[can_reply][always][1]=followers' [... other form fields ...]`
|
||||
|
||||
The JSON equivalent would be:
|
||||
|
||||
`curl -H 'Content-Type: application/json' -d '{"interaction_policy":{"can_reply":{"always":["author","followers"]}} [... other json fields ...]}'`
|
||||
|
||||
The server will perform some normalization on the submitted policy so that you can't submit something totally invalid.
|
||||
operationId: statusCreate
|
||||
parameters:
|
||||
- description: |-
|
||||
|
@ -8944,6 +8960,30 @@ paths:
|
|||
name: content_type
|
||||
type: string
|
||||
x-go-name: ContentType
|
||||
- description: Nth entry for interaction_policy.can_favourite.always.
|
||||
in: formData
|
||||
name: interaction_policy[can_favourite][always][0]
|
||||
type: string
|
||||
- description: Nth entry for interaction_policy.can_favourite.with_approval.
|
||||
in: formData
|
||||
name: interaction_policy[can_favourite][with_approval][0]
|
||||
type: string
|
||||
- description: Nth entry for interaction_policy.can_reply.always.
|
||||
in: formData
|
||||
name: interaction_policy[can_reply][always][0]
|
||||
type: string
|
||||
- description: Nth entry for interaction_policy.can_reply.with_approval.
|
||||
in: formData
|
||||
name: interaction_policy[can_reply][with_approval][0]
|
||||
type: string
|
||||
- description: Nth entry for interaction_policy.can_reblog.always.
|
||||
in: formData
|
||||
name: interaction_policy[can_reblog][always][0]
|
||||
type: string
|
||||
- description: Nth entry for interaction_policy.can_reblog.with_approval.
|
||||
in: formData
|
||||
name: interaction_policy[can_reblog][with_approval][0]
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
@ -8966,7 +9006,7 @@ paths:
|
|||
security:
|
||||
- OAuth2 Bearer:
|
||||
- write:statuses
|
||||
summary: Create a new status.
|
||||
summary: Create a new status using the given form field parameters.
|
||||
tags:
|
||||
- statuses
|
||||
/api/v1/statuses/{id}:
|
||||
|
|
|
@ -222,7 +222,7 @@ func (m *Module) PoliciesDefaultsPATCHHandler(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
form, err := parseUpdateAccountForm(c)
|
||||
form, err := parseUpdatePoliciesForm(c)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
|
@ -290,7 +290,7 @@ func customBind(
|
|||
return nil
|
||||
}
|
||||
|
||||
func parseUpdateAccountForm(c *gin.Context) (*apimodel.UpdateInteractionPoliciesRequest, error) {
|
||||
func parseUpdatePoliciesForm(c *gin.Context) (*apimodel.UpdateInteractionPoliciesRequest, error) {
|
||||
form := new(apimodel.UpdateInteractionPoliciesRequest)
|
||||
|
||||
switch ct := c.ContentType(); ct {
|
||||
|
|
|
@ -24,6 +24,8 @@ import (
|
|||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"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"
|
||||
|
@ -35,10 +37,27 @@ import (
|
|||
|
||||
// StatusCreatePOSTHandler swagger:operation POST /api/v1/statuses statusCreate
|
||||
//
|
||||
// Create a new status.
|
||||
// Create a new 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'.
|
||||
// The parameters can also be given in the body of the request, as XML, if the content-type is set to 'application/xml'.
|
||||
//
|
||||
// The 'interaction_policy' field can be used to set an interaction policy for this status.
|
||||
//
|
||||
// If submitting using form data, use the following pattern to set an interaction policy:
|
||||
//
|
||||
// `interaction_policy[INTERACTION_TYPE][CONDITION][INDEX]=Value`
|
||||
//
|
||||
// For example: `interaction_policy[can_reply][always][0]=author`
|
||||
//
|
||||
// Using `curl` this might look something like:
|
||||
//
|
||||
// `curl -F 'interaction_policy[can_reply][always][0]=author' -F 'interaction_policy[can_reply][always][1]=followers' [... other form fields ...]`
|
||||
//
|
||||
// The JSON equivalent would be:
|
||||
//
|
||||
// `curl -H 'Content-Type: application/json' -d '{"interaction_policy":{"can_reply":{"always":["author","followers"]}} [... other json fields ...]}'`
|
||||
//
|
||||
// The server will perform some normalization on the submitted policy so that you can't submit something totally invalid.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
|
@ -46,7 +65,6 @@ import (
|
|||
//
|
||||
// consumes:
|
||||
// - application/json
|
||||
// - application/xml
|
||||
// - application/x-www-form-urlencoded
|
||||
//
|
||||
// parameters:
|
||||
|
@ -181,6 +199,36 @@ import (
|
|||
// - text/plain
|
||||
// - text/markdown
|
||||
// in: formData
|
||||
// -
|
||||
// name: interaction_policy[can_favourite][always][0]
|
||||
// in: formData
|
||||
// description: Nth entry for interaction_policy.can_favourite.always.
|
||||
// type: string
|
||||
// -
|
||||
// name: interaction_policy[can_favourite][with_approval][0]
|
||||
// in: formData
|
||||
// description: Nth entry for interaction_policy.can_favourite.with_approval.
|
||||
// type: string
|
||||
// -
|
||||
// name: interaction_policy[can_reply][always][0]
|
||||
// in: formData
|
||||
// description: Nth entry for interaction_policy.can_reply.always.
|
||||
// type: string
|
||||
// -
|
||||
// name: interaction_policy[can_reply][with_approval][0]
|
||||
// in: formData
|
||||
// description: Nth entry for interaction_policy.can_reply.with_approval.
|
||||
// type: string
|
||||
// -
|
||||
// name: interaction_policy[can_reblog][always][0]
|
||||
// in: formData
|
||||
// description: Nth entry for interaction_policy.can_reblog.always.
|
||||
// type: string
|
||||
// -
|
||||
// name: interaction_policy[can_reblog][with_approval][0]
|
||||
// in: formData
|
||||
// description: Nth entry for interaction_policy.can_reblog.with_approval.
|
||||
// type: string
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
|
@ -223,8 +271,8 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
form := &apimodel.StatusCreateRequest{}
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
form, err := parseStatusCreateForm(c)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
@ -257,6 +305,75 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {
|
|||
c.JSON(http.StatusOK, apiStatus)
|
||||
}
|
||||
|
||||
// intPolicyFormBinding satisfies gin's binding.Binding interface.
|
||||
// Should only be used specifically for multipart/form-data MIME type.
|
||||
type intPolicyFormBinding struct{}
|
||||
|
||||
func (i intPolicyFormBinding) Name() string {
|
||||
return "InteractionPolicy"
|
||||
}
|
||||
|
||||
func (intPolicyFormBinding) Bind(req *http.Request, obj any) error {
|
||||
if err := req.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Change default namespace prefix and suffix to
|
||||
// allow correct parsing of the field attributes.
|
||||
decoder := form.NewDecoder()
|
||||
decoder.SetNamespacePrefix("[")
|
||||
decoder.SetNamespaceSuffix("]")
|
||||
|
||||
return decoder.Decode(obj, req.Form)
|
||||
}
|
||||
|
||||
func parseStatusCreateForm(c *gin.Context) (*apimodel.StatusCreateRequest, error) {
|
||||
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
|
||||
}
|
||||
|
||||
case binding.MIMEPOSTForm:
|
||||
// Bind with default form binding first.
|
||||
if err := c.ShouldBindWith(form, binding.FormPost); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Now do custom binding.
|
||||
intReqForm := new(apimodel.StatusInteractionPolicyForm)
|
||||
if err := c.ShouldBindWith(intReqForm, intPolicyFormBinding{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// Now do custom binding.
|
||||
intReqForm := new(apimodel.StatusInteractionPolicyForm)
|
||||
if err := c.ShouldBindWith(intReqForm, intPolicyFormBinding{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// validateNormalizeCreateStatus checks the form
|
||||
// for disallowed combinations of attachments and
|
||||
// overlength inputs.
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -196,33 +196,44 @@ 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" xml:"status"`
|
||||
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" xml:"media_ids"`
|
||||
MediaIDs []string `form:"media_ids[]" json:"media_ids"`
|
||||
// Poll to include with this status.
|
||||
Poll *PollRequest `form:"poll" json:"poll" xml:"poll"`
|
||||
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" xml:"in_reply_to_id"`
|
||||
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" xml:"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" xml:"spoiler_text"`
|
||||
SpoilerText string `form:"spoiler_text" json:"spoiler_text"`
|
||||
// Visibility of the posted status.
|
||||
Visibility Visibility `form:"visibility" json:"visibility" xml:"visibility"`
|
||||
Visibility Visibility `form:"visibility" json:"visibility"`
|
||||
// Set to "true" if this status should not be federated, ie. it should be a "local only" status.
|
||||
LocalOnly *bool `form:"local_only"`
|
||||
LocalOnly *bool `form:"local_only" json:"local_only"`
|
||||
// Deprecated: Only used if LocalOnly is not set.
|
||||
Federated *bool `form:"federated"`
|
||||
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" xml:"scheduled_at"`
|
||||
ScheduledAt string `form:"scheduled_at" json:"scheduled_at"`
|
||||
// ISO 639 language code for this status.
|
||||
Language string `form:"language" json:"language" xml:"language"`
|
||||
Language string `form:"language" json:"language"`
|
||||
// Content type to use when parsing this status.
|
||||
ContentType StatusContentType `form:"content_type" json:"content_type" xml:"content_type"`
|
||||
ContentType StatusContentType `form:"content_type" json:"content_type"`
|
||||
// Interaction policy to use for this status.
|
||||
InteractionPolicy *InteractionPolicy `form:"-" json:"interaction_policy"`
|
||||
}
|
||||
|
||||
// Separate form for parsing interaction
|
||||
// policy on status create requests.
|
||||
//
|
||||
// swagger:ignore
|
||||
type StatusInteractionPolicyForm struct {
|
||||
// Interaction policy to use for this status.
|
||||
InteractionPolicy *InteractionPolicy `form:"interaction_policy" json:"-"`
|
||||
}
|
||||
|
||||
// Visibility models the visibility of a status.
|
||||
|
|
|
@ -117,14 +117,14 @@ func (p *Processor) Create(
|
|||
return nil, errWithCode
|
||||
}
|
||||
|
||||
if err := processVisibility(form, requester.Settings.Privacy, status); err != nil {
|
||||
if err := p.processVisibility(ctx, form, requester.Settings.Privacy, status); err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// Process policy AFTER visibility as it
|
||||
// relies on status.Visibility being set.
|
||||
if err := processInteractionPolicy(form, requester.Settings, status); err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
// Process policy AFTER visibility as it relies
|
||||
// on status.Visibility and form.Visibility being set.
|
||||
if errWithCode := processInteractionPolicy(form, requester.Settings, status); errWithCode != nil {
|
||||
return nil, errWithCode
|
||||
}
|
||||
|
||||
if err := processLanguage(form, requester.Settings.Language, status); err != nil {
|
||||
|
@ -337,7 +337,8 @@ func (p *Processor) processMediaIDs(ctx context.Context, form *apimodel.StatusCr
|
|||
return nil
|
||||
}
|
||||
|
||||
func processVisibility(
|
||||
func (p *Processor) processVisibility(
|
||||
ctx context.Context,
|
||||
form *apimodel.StatusCreateRequest,
|
||||
accountDefaultVis gtsmodel.Visibility,
|
||||
status *gtsmodel.Status,
|
||||
|
@ -347,13 +348,17 @@ func processVisibility(
|
|||
case form.Visibility != "":
|
||||
status.Visibility = typeutils.APIVisToVis(form.Visibility)
|
||||
|
||||
// Fall back to account default.
|
||||
// Fall back to account default, set
|
||||
// this back on the form for later use.
|
||||
case accountDefaultVis != "":
|
||||
status.Visibility = accountDefaultVis
|
||||
form.Visibility = p.converter.VisToAPIVis(ctx, accountDefaultVis)
|
||||
|
||||
// What? Fall back to global default.
|
||||
// What? Fall back to global default, set
|
||||
// this back on the form for later use.
|
||||
default:
|
||||
status.Visibility = gtsmodel.VisibilityDefault
|
||||
form.Visibility = p.converter.VisToAPIVis(ctx, gtsmodel.VisibilityDefault)
|
||||
}
|
||||
|
||||
// Set federated according to "local_only" field,
|
||||
|
@ -365,17 +370,32 @@ func processVisibility(
|
|||
}
|
||||
|
||||
func processInteractionPolicy(
|
||||
_ *apimodel.StatusCreateRequest,
|
||||
form *apimodel.StatusCreateRequest,
|
||||
settings *gtsmodel.AccountSettings,
|
||||
status *gtsmodel.Status,
|
||||
) error {
|
||||
// TODO: parse policy for this
|
||||
// status from form and prefer this.
|
||||
) gtserror.WithCode {
|
||||
|
||||
// If policy is set on the
|
||||
// form then prefer this.
|
||||
//
|
||||
// TODO: prevent scope widening by
|
||||
// limiting interaction policy if
|
||||
// inReplyTo status has a stricter
|
||||
// interaction policy than this one.
|
||||
if form.InteractionPolicy != nil {
|
||||
p, err := typeutils.APIInteractionPolicyToInteractionPolicy(
|
||||
form.InteractionPolicy,
|
||||
form.Visibility,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
errWithCode := gtserror.NewErrorBadRequest(err, err.Error())
|
||||
return errWithCode
|
||||
}
|
||||
|
||||
status.InteractionPolicy = p
|
||||
return nil
|
||||
}
|
||||
|
||||
switch status.Visibility {
|
||||
|
||||
|
|
Loading…
Reference in a new issue