summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2024-09-18 18:35:35 +0200
committerLibravatar GitHub <noreply@github.com>2024-09-18 16:35:35 +0000
commitc378ad2bb3ae9ea2877baf167d6a1397675eff17 (patch)
tree0feda9ab0ce2c6798b73445fbfede7fde908956f
parent[chore] make csv export ordering determinate (#3318) (diff)
downloadgotosocial-c378ad2bb3ae9ea2877baf167d6a1397675eff17.tar.xz
[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
-rw-r--r--docs/api/swagger.yaml46
-rw-r--r--internal/api/client/interactionpolicies/updatedefaults.go4
-rw-r--r--internal/api/client/statuses/statuscreate.go127
-rw-r--r--internal/api/client/statuses/statuscreate_test.go1503
-rw-r--r--internal/api/model/status.go35
-rw-r--r--internal/processing/status/create.go44
6 files changed, 1344 insertions, 415 deletions
diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml
index 3db6acb99..54d318111 100644
--- a/docs/api/swagger.yaml
+++ b/docs/api/swagger.yaml
@@ -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}:
diff --git a/internal/api/client/interactionpolicies/updatedefaults.go b/internal/api/client/interactionpolicies/updatedefaults.go
index e11a3bd19..39e95784f 100644
--- a/internal/api/client/interactionpolicies/updatedefaults.go
+++ b/internal/api/client/interactionpolicies/updatedefaults.go
@@ -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 {
diff --git a/internal/api/client/statuses/statuscreate.go b/internal/api/client/statuses/statuscreate.go
index de1581515..996f7605b 100644
--- a/internal/api/client/statuses/statuscreate.go
+++ b/internal/api/client/statuses/statuscreate.go
@@ -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.
diff --git a/internal/api/client/statuses/statuscreate_test.go b/internal/api/client/statuses/statuscreate_test.go
index 94d91c8b9..d32feb6c7 100644
--- a/internal/api/client/statuses/statuscreate_test.go
+++ b/internal/api/client/statuses/statuscreate_test.go
@@ -18,22 +18,20 @@
package statuses_test
import (
+ "bytes"
"context"
"encoding/json"
"fmt"
"io"
- "io/ioutil"
"net/http"
"net/http/httptest"
- "net/url"
"strings"
"testing"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/api/client/statuses"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -45,459 +43,1202 @@ type StatusCreateTestSuite struct {
const (
statusWithLinksAndTags = "#test alright, should be able to post #links with fragments in them now, let's see........\n\nhttps://docs.gotosocial.org/en/latest/user_guide/posts/#links\n\n#gotosocial\n\n(tobi remember to pull the docker image challenge)"
statusMarkdown = "# Title\n\n## Smaller title\n\nThis is a post written in [markdown](https://www.markdownguide.org/)\n\n<img src=\"https://d33wubrfki0l68.cloudfront.net/f1f475a6fda1c2c4be4cac04033db5c3293032b4/513a4/assets/images/markdown-mark-white.svg\"/>"
- statusMarkdownExpected = "<h1>Title</h1><h2>Smaller title</h2><p>This is a post written in <a href=\"https://www.markdownguide.org/\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">markdown</a></p>"
)
-// Post a new status with some custom visibility settings
-func (suite *StatusCreateTestSuite) TestPostNewStatus() {
- t := suite.testTokens["local_account_1"]
- oauthToken := oauth.DBTokenToToken(t)
-
- // setup
+func (suite *StatusCreateTestSuite) postStatus(
+ formData map[string][]string,
+ jsonData string,
+) (string, *httptest.ResponseRecorder) {
recorder := httptest.NewRecorder()
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
- ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
+ ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"]))
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", statuses.BasePath), nil) // the endpoint we're hitting
- ctx.Request.Header.Set("accept", "application/json")
- ctx.Request.Form = url.Values{
- "status": {"this is a brand new status! #helloworld"},
- "spoiler_text": {"hello hello"},
- "sensitive": {"true"},
- "visibility": {string(apimodel.VisibilityMutualsOnly)},
+
+ if formData != nil {
+ buf, w, err := testrig.CreateMultipartFormData(nil, formData)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ ctx.Request = httptest.NewRequest(
+ http.MethodPost,
+ "http://localhost:8080"+statuses.BasePath,
+ bytes.NewReader(buf.Bytes()),
+ )
+ ctx.Request.Header.Set("content-type", w.FormDataContentType())
+ } else {
+ ctx.Request = httptest.NewRequest(
+ http.MethodPost,
+ "http://localhost:8080"+statuses.BasePath,
+ bytes.NewReader([]byte(jsonData)),
+ )
+ ctx.Request.Header.Set("content-type", "application/json")
}
- suite.statusModule.StatusCreatePOSTHandler(ctx)
- // check response
+ ctx.Request.Header.Set("accept", "application/json")
- // 1. we should have OK from our call to the function
- suite.EqualValues(http.StatusOK, recorder.Code)
+ // Trigger handler.
+ suite.statusModule.StatusCreatePOSTHandler(ctx)
result := recorder.Result()
defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- suite.NoError(err)
-
- statusReply := &apimodel.Status{}
- err = json.Unmarshal(b, statusReply)
- suite.NoError(err)
-
- suite.Equal("hello hello", statusReply.SpoilerText)
- suite.Equal("<p>this is a brand new status! <a href=\"http://localhost:8080/tags/helloworld\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>helloworld</span></a></p>", statusReply.Content)
- suite.True(statusReply.Sensitive)
- suite.Equal(apimodel.VisibilityPrivate, statusReply.Visibility) // even though we set this status to mutuals only, it should serialize to private, because the mastodon api has no idea about mutuals_only
- suite.Len(statusReply.Tags, 1)
- suite.Equal(apimodel.Tag{
- Name: "helloworld",
- URL: "http://localhost:8080/tags/helloworld",
- }, statusReply.Tags[0])
-
- gtsTag := &gtsmodel.Tag{}
- err = suite.db.GetWhere(context.Background(), []db.Where{{Key: "name", Value: "helloworld"}}, gtsTag)
- suite.NoError(err)
-}
-func (suite *StatusCreateTestSuite) TestPostNewStatusMarkdown() {
- // Copy zork.
- testAccount := &gtsmodel.Account{}
- *testAccount = *suite.testAccounts["local_account_1"]
-
- // Copy zork's settings.
- settings := &gtsmodel.AccountSettings{}
- *settings = *suite.testAccounts["local_account_1"].Settings
- testAccount.Settings = settings
-
- // set default post language of zork to markdown
- testAccount.Settings.StatusContentType = "text/markdown"
- err := suite.db.UpdateAccountSettings(context.Background(), testAccount.Settings)
+ data, err := io.ReadAll(result.Body)
if err != nil {
suite.FailNow(err.Error())
}
- suite.Equal(testAccount.Settings.StatusContentType, "text/markdown")
-
- t := suite.testTokens["local_account_1"]
- oauthToken := oauth.DBTokenToToken(t)
- recorder := httptest.NewRecorder()
- ctx, _ := testrig.CreateGinTestContext(recorder, nil)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
- ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
- ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
- ctx.Set(oauth.SessionAuthorizedAccount, testAccount)
-
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", statuses.BasePath), nil)
- ctx.Request.Header.Set("accept", "application/json")
- ctx.Request.Form = url.Values{
- "status": {statusMarkdown},
- "visibility": {string(apimodel.VisibilityPublic)},
+ rawMap := make(map[string]any)
+ if err := json.Unmarshal(data, &rawMap); err != nil {
+ suite.FailNow(err.Error())
}
- suite.statusModule.StatusCreatePOSTHandler(ctx)
- suite.EqualValues(http.StatusOK, recorder.Code)
+ // Replace any fields from the raw map that
+ // aren't determinate (date, id, url, etc).
+ if _, ok := rawMap["id"]; ok {
+ rawMap["id"] = id.Highest
+ }
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- suite.NoError(err)
+ if _, ok := rawMap["uri"]; ok {
+ rawMap["uri"] = "http://localhost:8080/some/determinate/url"
+ }
- statusReply := &apimodel.Status{}
- err = json.Unmarshal(b, statusReply)
- suite.NoError(err)
+ if _, ok := rawMap["url"]; ok {
+ rawMap["url"] = "http://localhost:8080/some/determinate/url"
+ }
- suite.Equal(statusMarkdownExpected, statusReply.Content)
-}
+ if _, ok := rawMap["created_at"]; ok {
+ rawMap["created_at"] = "right the hell just now babyee"
+ }
-// mention an account that is not yet known to the instance -- it should be looked up and put in the db
-func (suite *StatusCreateTestSuite) TestMentionUnknownAccount() {
- // first remove remote account 1 from the database so it gets looked up again
- remoteAccount := suite.testAccounts["remote_account_1"]
- err := suite.db.DeleteAccount(context.Background(), remoteAccount.ID)
- suite.NoError(err)
+ // Make ID of any mentions determinate.
+ if menchiesRaw, ok := rawMap["mentions"]; ok {
+ menchies, ok := menchiesRaw.([]any)
+ if !ok {
+ suite.FailNow("couldn't coerce menchies")
+ }
- t := suite.testTokens["local_account_1"]
- oauthToken := oauth.DBTokenToToken(t)
+ for _, menchieRaw := range menchies {
+ menchie, ok := menchieRaw.(map[string]any)
+ if !ok {
+ suite.FailNow("couldn't coerce menchie")
+ }
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := testrig.CreateGinTestContext(recorder, nil)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
- ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
- ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
- ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", statuses.BasePath), nil) // the endpoint we're hitting
- ctx.Request.Header.Set("accept", "application/json")
- ctx.Request.Form = url.Values{
- "status": {"hello @brand_new_person@unknown-instance.com"},
- "visibility": {string(apimodel.VisibilityPublic)},
+ if _, ok := menchie["id"]; ok {
+ menchie["id"] = id.Highest
+ }
+ }
}
- suite.statusModule.StatusCreatePOSTHandler(ctx)
- suite.EqualValues(http.StatusOK, recorder.Code)
+ // Make fields of any poll determinate.
+ if pollRaw, ok := rawMap["poll"]; ok && pollRaw != nil {
+ poll, ok := pollRaw.(map[string]any)
+ if !ok {
+ suite.FailNow("couldn't coerce poll")
+ }
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- suite.NoError(err)
+ if _, ok := poll["id"]; ok {
+ poll["id"] = id.Highest
+ }
- statusReply := &apimodel.Status{}
- err = json.Unmarshal(b, statusReply)
- suite.NoError(err)
+ if _, ok := poll["expires_at"]; ok {
+ poll["expires_at"] = "ah like you know whatever dude it's chill"
+ }
+ }
- // if the status is properly formatted, that means the account has been put in the db
- suite.Equal(`<p>hello <span class="h-card"><a href="https://unknown-instance.com/@brand_new_person" class="u-url mention" rel="nofollow noreferrer noopener" target="_blank">@<span>brand_new_person</span></a></span></p>`, statusReply.Content)
- suite.Equal(apimodel.VisibilityPublic, statusReply.Visibility)
-}
+ // Replace account since that's not really
+ // what we care about for these tests.
+ if _, ok := rawMap["account"]; ok {
+ rawMap["account"] = "yeah this is my account, what about it punk"
+ }
-func (suite *StatusCreateTestSuite) TestPostAnotherNewStatus() {
- t := suite.testTokens["local_account_1"]
- oauthToken := oauth.DBTokenToToken(t)
+ // For readability, don't
+ // escape HTML, and indent json.
+ out := new(bytes.Buffer)
+ enc := json.NewEncoder(out)
+ enc.SetEscapeHTML(false)
+ enc.SetIndent("", " ")
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := testrig.CreateGinTestContext(recorder, nil)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
- ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
- ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
- ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", statuses.BasePath), nil) // the endpoint we're hitting
- ctx.Request.Header.Set("accept", "application/json")
- ctx.Request.Form = url.Values{
- "status": {statusWithLinksAndTags},
+ if err := enc.Encode(&rawMap); err != nil {
+ suite.FailNow(err.Error())
}
- suite.statusModule.StatusCreatePOSTHandler(ctx)
- // check response
+ return strings.TrimSpace(out.String()), recorder
+}
- // 1. we should have OK from our call to the function
- suite.EqualValues(http.StatusOK, recorder.Code)
+// Post a new status with some custom visibility settings
+func (suite *StatusCreateTestSuite) TestPostNewStatus() {
+ out, recorder := suite.postStatus(map[string][]string{
+ "status": {"this is a brand new status! #helloworld"},
+ "spoiler_text": {"hello hello"},
+ "sensitive": {"true"},
+ "visibility": {string(apimodel.VisibilityMutualsOnly)},
+ }, "")
+
+ // We should have OK from
+ // our call to the function.
+ suite.Equal(http.StatusOK, recorder.Code)
+ suite.Equal(`{
+ "account": "yeah this is my account, what about it punk",
+ "application": {
+ "name": "really cool gts application",
+ "website": "https://reallycool.app"
+ },
+ "bookmarked": false,
+ "card": null,
+ "content": "<p>this is a brand new status! <a href=\"http://localhost:8080/tags/helloworld\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>helloworld</span></a></p>",
+ "created_at": "right the hell just now babyee",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 0,
+ "id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "interaction_policy": {
+ "can_favourite": {
+ "always": [
+ "author",
+ "followers",
+ "mentioned",
+ "me"
+ ],
+ "with_approval": []
+ },
+ "can_reblog": {
+ "always": [
+ "author",
+ "me"
+ ],
+ "with_approval": []
+ },
+ "can_reply": {
+ "always": [
+ "author",
+ "followers",
+ "mentioned",
+ "me"
+ ],
+ "with_approval": []
+ }
+ },
+ "language": "en",
+ "media_attachments": [],
+ "mentions": [],
+ "muted": false,
+ "pinned": false,
+ "poll": null,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": true,
+ "spoiler_text": "hello hello",
+ "tags": [
+ {
+ "name": "helloworld",
+ "url": "http://localhost:8080/tags/helloworld"
+ }
+ ],
+ "text": "this is a brand new status! #helloworld",
+ "uri": "http://localhost:8080/some/determinate/url",
+ "url": "http://localhost:8080/some/determinate/url",
+ "visibility": "private"
+}`, out)
+}
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- suite.NoError(err)
+// Post a new status with some custom visibility settings
+func (suite *StatusCreateTestSuite) TestPostNewStatusIntPolicy() {
+ out, recorder := suite.postStatus(map[string][]string{
+ "status": {"this is a brand new status! #helloworld"},
+ "interaction_policy[can_reply][always][0]": {"author"},
+ "interaction_policy[can_reply][always][1]": {"followers"},
+ "interaction_policy[can_reply][always][2]": {"following"},
+ "interaction_policy[can_reply][with_approval][0]": {"public"},
+ "interaction_policy[can_announce][always][0]": {""},
+ }, "")
+
+ // We should have OK from
+ // our call to the function.
+ suite.Equal(http.StatusOK, recorder.Code)
+
+ // Custom interaction policies
+ // should be set on the status.
+ suite.Equal(`{
+ "account": "yeah this is my account, what about it punk",
+ "application": {
+ "name": "really cool gts application",
+ "website": "https://reallycool.app"
+ },
+ "bookmarked": false,
+ "card": null,
+ "content": "<p>this is a brand new status! <a href=\"http://localhost:8080/tags/helloworld\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>helloworld</span></a></p>",
+ "created_at": "right the hell just now babyee",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 0,
+ "id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "interaction_policy": {
+ "can_favourite": {
+ "always": [
+ "author",
+ "me"
+ ],
+ "with_approval": []
+ },
+ "can_reblog": {
+ "always": [
+ "author",
+ "me"
+ ],
+ "with_approval": []
+ },
+ "can_reply": {
+ "always": [
+ "author",
+ "followers",
+ "following",
+ "mentioned",
+ "me"
+ ],
+ "with_approval": [
+ "public"
+ ]
+ }
+ },
+ "language": "en",
+ "media_attachments": [],
+ "mentions": [],
+ "muted": false,
+ "pinned": false,
+ "poll": null,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [
+ {
+ "name": "helloworld",
+ "url": "http://localhost:8080/tags/helloworld"
+ }
+ ],
+ "text": "this is a brand new status! #helloworld",
+ "uri": "http://localhost:8080/some/determinate/url",
+ "url": "http://localhost:8080/some/determinate/url",
+ "visibility": "public"
+}`, out)
+}
- statusReply := &apimodel.Status{}
- err = json.Unmarshal(b, statusReply)
- suite.NoError(err)
+func (suite *StatusCreateTestSuite) TestPostNewStatusIntPolicyJSON() {
+ out, recorder := suite.postStatus(nil, `{
+ "status": "this is a brand new status! #helloworld",
+ "interaction_policy": {
+ "can_reply": {
+ "always": [
+ "author",
+ "followers",
+ "following"
+ ],
+ "with_approval": [
+ "public"
+ ]
+ },
+ "can_announce": {
+ "always": []
+ }
+ }
+}`)
+
+ // We should have OK from
+ // our call to the function.
+ suite.Equal(http.StatusOK, recorder.Code)
+
+ // Custom interaction policies
+ // should be set on the status.
+ suite.Equal(`{
+ "account": "yeah this is my account, what about it punk",
+ "application": {
+ "name": "really cool gts application",
+ "website": "https://reallycool.app"
+ },
+ "bookmarked": false,
+ "card": null,
+ "content": "<p>this is a brand new status! <a href=\"http://localhost:8080/tags/helloworld\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>helloworld</span></a></p>",
+ "created_at": "right the hell just now babyee",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 0,
+ "id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "interaction_policy": {
+ "can_favourite": {
+ "always": [
+ "author",
+ "me"
+ ],
+ "with_approval": []
+ },
+ "can_reblog": {
+ "always": [
+ "author",
+ "me"
+ ],
+ "with_approval": []
+ },
+ "can_reply": {
+ "always": [
+ "author",
+ "followers",
+ "following",
+ "mentioned",
+ "me"
+ ],
+ "with_approval": [
+ "public"
+ ]
+ }
+ },
+ "language": "en",
+ "media_attachments": [],
+ "mentions": [],
+ "muted": false,
+ "pinned": false,
+ "poll": null,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [
+ {
+ "name": "helloworld",
+ "url": "http://localhost:8080/tags/helloworld"
+ }
+ ],
+ "text": "this is a brand new status! #helloworld",
+ "uri": "http://localhost:8080/some/determinate/url",
+ "url": "http://localhost:8080/some/determinate/url",
+ "visibility": "public"
+}`, out)
+}
- suite.Equal("<p><a href=\"http://localhost:8080/tags/test\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>test</span></a> alright, should be able to post <a href=\"http://localhost:8080/tags/links\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>links</span></a> with fragments in them now, let's see........<br><br><a href=\"https://docs.gotosocial.org/en/latest/user_guide/posts/#links\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">https://docs.gotosocial.org/en/latest/user_guide/posts/#links</a><br><br><a href=\"http://localhost:8080/tags/gotosocial\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>gotosocial</span></a><br><br>(tobi remember to pull the docker image challenge)</p>", statusReply.Content)
+func (suite *StatusCreateTestSuite) TestPostNewStatusMessedUpIntPolicy() {
+ out, recorder := suite.postStatus(nil, `{
+ "status": "this is a brand new status! #helloworld",
+ "visibility": "followers_only",
+ "interaction_policy": {
+ "can_reply": {
+ "always": [
+ "public"
+ ]
+ }
+ }
+}`)
+
+ // We should have 400 from
+ // our call to the function.
+ suite.Equal(http.StatusBadRequest, recorder.Code)
+
+ // We should have a helpful error
+ // message telling us how we screwed up.
+ suite.Equal(`{
+ "error": "Bad Request: error converting followers_only.can_reply.always: policyURI public is not feasible for visibility followers_only"
+}`, out)
}
-func (suite *StatusCreateTestSuite) TestPostNewStatusWithEmoji() {
- t := suite.testTokens["local_account_1"]
- oauthToken := oauth.DBTokenToToken(t)
+func (suite *StatusCreateTestSuite) TestPostNewStatusMarkdown() {
+ out, recorder := suite.postStatus(map[string][]string{
+ "status": {statusMarkdown},
+ "visibility": {string(apimodel.VisibilityPublic)},
+ "content_type": {"text/markdown"},
+ }, "")
+
+ // We should have OK from
+ // our call to the function.
+ suite.Equal(http.StatusOK, recorder.Code)
+
+ // The content field should have
+ // all the nicely parsed markdown stuff.
+ suite.Equal(`{
+ "account": "yeah this is my account, what about it punk",
+ "application": {
+ "name": "really cool gts application",
+ "website": "https://reallycool.app"
+ },
+ "bookmarked": false,
+ "card": null,
+ "content": "<h1>Title</h1><h2>Smaller title</h2><p>This is a post written in <a href=\"https://www.markdownguide.org/\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">markdown</a></p>",
+ "created_at": "right the hell just now babyee",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 0,
+ "id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "interaction_policy": {
+ "can_favourite": {
+ "always": [
+ "public",
+ "me"
+ ],
+ "with_approval": []
+ },
+ "can_reblog": {
+ "always": [
+ "public",
+ "me"
+ ],
+ "with_approval": []
+ },
+ "can_reply": {
+ "always": [
+ "public",
+ "me"
+ ],
+ "with_approval": []
+ }
+ },
+ "language": "en",
+ "media_attachments": [],
+ "mentions": [],
+ "muted": false,
+ "pinned": false,
+ "poll": null,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "text": "# Title\n\n## Smaller title\n\nThis is a post written in [markdown](https://www.markdownguide.org/)\n\n<img src=\"https://d33wubrfki0l68.cloudfront.net/f1f475a6fda1c2c4be4cac04033db5c3293032b4/513a4/assets/images/markdown-mark-white.svg\"/>",
+ "uri": "http://localhost:8080/some/determinate/url",
+ "url": "http://localhost:8080/some/determinate/url",
+ "visibility": "public"
+}`, out)
+}
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := testrig.CreateGinTestContext(recorder, nil)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
- ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
- ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
- ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", statuses.BasePath), nil) // the endpoint we're hitting
- ctx.Request.Header.Set("accept", "application/json")
- ctx.Request.Form = url.Values{
- "status": {"here is a rainbow emoji a few times! :rainbow: :rainbow: :rainbow: \n here's an emoji that isn't in the db: :test_emoji: "},
+// Mention an account that is not yet known to the
+// instance -- it should be looked up and put in the db.
+func (suite *StatusCreateTestSuite) TestMentionUnknownAccount() {
+ // First remove remote account 1 from the database
+ // so it gets looked up again when we mention it.
+ remoteAccount := suite.testAccounts["remote_account_1"]
+ if err := suite.db.DeleteAccount(
+ context.Background(),
+ remoteAccount.ID,
+ ); err != nil {
+ suite.FailNow(err.Error())
}
- suite.statusModule.StatusCreatePOSTHandler(ctx)
- suite.EqualValues(http.StatusOK, recorder.Code)
-
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- suite.NoError(err)
-
- statusReply := &apimodel.Status{}
- err = json.Unmarshal(b, statusReply)
- suite.NoError(err)
-
- suite.Equal("", statusReply.SpoilerText)
- suite.Equal("<p>here is a rainbow emoji a few times! :rainbow: :rainbow: :rainbow:<br>here's an emoji that isn't in the db: :test_emoji:</p>", statusReply.Content)
+ out, recorder := suite.postStatus(map[string][]string{
+ "status": {"hello @brand_new_person@unknown-instance.com"},
+ "visibility": {string(apimodel.VisibilityPublic)},
+ }, "")
+
+ // We should have OK from
+ // our call to the function.
+ suite.Equal(http.StatusOK, recorder.Code)
+
+ // Status should have a mention of
+ // the now-freshly-looked-up account.
+ suite.Equal(`{
+ "account": "yeah this is my account, what about it punk",
+ "application": {
+ "name": "really cool gts application",
+ "website": "https://reallycool.app"
+ },
+ "bookmarked": false,
+ "card": null,
+ "content": "<p>hello <span class=\"h-card\"><a href=\"https://unknown-instance.com/@brand_new_person\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>brand_new_person</span></a></span></p>",
+ "created_at": "right the hell just now babyee",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 0,
+ "id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "interaction_policy": {
+ "can_favourite": {
+ "always": [
+ "public",
+ "me"
+ ],
+ "with_approval": []
+ },
+ "can_reblog": {
+ "always": [
+ "public",
+ "me"
+ ],
+ "with_approval": []
+ },
+ "can_reply": {
+ "always": [
+ "public",
+ "me"
+ ],
+ "with_approval": []
+ }
+ },
+ "language": "en",
+ "media_attachments": [],
+ "mentions": [
+ {
+ "acct": "brand_new_person@unknown-instance.com",
+ "id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
+ "url": "https://unknown-instance.com/@brand_new_person",
+ "username": "brand_new_person"
+ }
+ ],
+ "muted": false,
+ "pinned": false,
+ "poll": null,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "text": "hello @brand_new_person@unknown-instance.com",
+ "uri": "http://localhost:8080/some/determinate/url",
+ "url": "http://localhost:8080/some/determinate/url",
+ "visibility": "public"
+}`, out)
+}
- suite.Len(statusReply.Emojis, 1)
- apiEmoji := statusReply.Emojis[0]
- gtsEmoji := testrig.NewTestEmojis()["rainbow"]
+func (suite *StatusCreateTestSuite) TestPostStatusWithLinksAndTags() {
+ out, recorder := suite.postStatus(map[string][]string{
+ "status": {statusWithLinksAndTags},
+ }, "")
+
+ // We should have OK from
+ // our call to the function.
+ suite.Equal(http.StatusOK, recorder.Code)
+
+ // Status should have proper
+ // tags + formatted links.
+ suite.Equal(`{
+ "account": "yeah this is my account, what about it punk",
+ "application": {
+ "name": "really cool gts application",
+ "website": "https://reallycool.app"
+ },
+ "bookmarked": false,
+ "card": null,
+ "content": "<p><a href=\"http://localhost:8080/tags/test\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>test</span></a> alright, should be able to post <a href=\"http://localhost:8080/tags/links\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>links</span></a> with fragments in them now, let's see........<br><br><a href=\"https://docs.gotosocial.org/en/latest/user_guide/posts/#links\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">https://docs.gotosocial.org/en/latest/user_guide/posts/#links</a><br><br><a href=\"http://localhost:8080/tags/gotosocial\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>gotosocial</span></a><br><br>(tobi remember to pull the docker image challenge)</p>",
+ "created_at": "right the hell just now babyee",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 0,
+ "id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "interaction_policy": {
+ "can_favourite": {
+ "always": [
+ "public",
+ "me"
+ ],
+ "with_approval": []
+ },
+ "can_reblog": {
+ "always": [
+ "public",
+ "me"
+ ],
+ "with_approval": []
+ },
+ "can_reply": {
+ "always": [
+ "public",
+ "me"
+ ],
+ "with_approval": []
+ }
+ },
+ "language": "en",
+ "media_attachments": [],
+ "mentions": [],
+ "muted": false,
+ "pinned": false,
+ "poll": null,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [
+ {
+ "name": "test",
+ "url": "http://localhost:8080/tags/test"
+ },
+ {
+ "name": "links",
+ "url": "http://localhost:8080/tags/links"
+ },
+ {
+ "name": "gotosocial",
+ "url": "http://localhost:8080/tags/gotosocial"
+ }
+ ],
+ "text": "#test alright, should be able to post #links with fragments in them now, let's see........\n\nhttps://docs.gotosocial.org/en/latest/user_guide/posts/#links\n\n#gotosocial\n\n(tobi remember to pull the docker image challenge)",
+ "uri": "http://localhost:8080/some/determinate/url",
+ "url": "http://localhost:8080/some/determinate/url",
+ "visibility": "public"
+}`, out)
+}
- suite.Equal(gtsEmoji.Shortcode, apiEmoji.Shortcode)
- suite.Equal(gtsEmoji.ImageURL, apiEmoji.URL)
- suite.Equal(gtsEmoji.ImageStaticURL, apiEmoji.StaticURL)
+func (suite *StatusCreateTestSuite) TestPostNewStatusWithEmoji() {
+ out, recorder := suite.postStatus(map[string][]string{
+ "status": {"here is a rainbow emoji a few times! :rainbow: :rainbow: :rainbow: \n here's an emoji that isn't in the db: :test_emoji: "},
+ }, "")
+
+ // We should have OK from
+ // our call to the function.
+ suite.Equal(http.StatusOK, recorder.Code)
+
+ // Emojis array should be
+ // populated on returned status.
+ suite.Equal(`{
+ "account": "yeah this is my account, what about it punk",
+ "application": {
+ "name": "really cool gts application",
+ "website": "https://reallycool.app"
+ },
+ "bookmarked": false,
+ "card": null,
+ "content": "<p>here is a rainbow emoji a few times! :rainbow: :rainbow: :rainbow:<br>here's an emoji that isn't in the db: :test_emoji:</p>",
+ "created_at": "right the hell just now babyee",
+ "emojis": [
+ {
+ "category": "reactions",
+ "shortcode": "rainbow",
+ "static_url": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/static/01F8MH9H8E4VG3KDYJR9EGPXCQ.png",
+ "url": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png",
+ "visible_in_picker": true
+ }
+ ],
+ "favourited": false,
+ "favourites_count": 0,
+ "id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "interaction_policy": {
+ "can_favourite": {
+ "always": [
+ "public",
+ "me"
+ ],
+ "with_approval": []
+ },
+ "can_reblog": {
+ "always": [
+ "public",
+ "me"
+ ],
+ "with_approval": []
+ },
+ "can_reply": {
+ "always": [
+ "public",
+ "me"
+ ],
+ "with_approval": []
+ }
+ },
+ "language": "en",
+ "media_attachments": [],
+ "mentions": [],
+ "muted": false,
+ "pinned": false,
+ "poll": null,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "text": "here is a rainbow emoji a few times! :rainbow: :rainbow: :rainbow: \n here's an emoji that isn't in the db: :test_emoji: ",
+ "uri": "http://localhost:8080/some/determinate/url",
+ "url": "http://localhost:8080/some/determinate/url",
+ "visibility": "public"
+}`, out)
}
// Try to reply to a status that doesn't exist
func (suite *StatusCreateTestSuite) TestReplyToNonexistentStatus() {
- t := suite.testTokens["local_account_1"]
- oauthToken := oauth.DBTokenToToken(t)
-
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := testrig.CreateGinTestContext(recorder, nil)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
- ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
- ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
- ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", statuses.BasePath), nil) // the endpoint we're hitting
- ctx.Request.Header.Set("accept", "application/json")
- ctx.Request.Form = url.Values{
+ out, recorder := suite.postStatus(map[string][]string{
"status": {"this is a reply to a status that doesn't exist"},
"spoiler_text": {"don't open cuz it won't work"},
"in_reply_to_id": {"3759e7ef-8ee1-4c0c-86f6-8b70b9ad3d50"},
- }
- suite.statusModule.StatusCreatePOSTHandler(ctx)
-
- // check response
-
- suite.EqualValues(http.StatusNotFound, recorder.Code)
-
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- suite.NoError(err)
- suite.Equal(`{"error":"Not Found: target status not found"}`, string(b))
+ }, "")
+
+ // We should have 404 from
+ // our call to the function.
+ suite.Equal(http.StatusNotFound, recorder.Code)
+ suite.Equal(`{
+ "error": "Not Found: target status not found"
+}`, out)
}
-// Post a reply to the status of a local user that allows replies.
+// Post a reply to the status of
+// a local user that allows replies.
func (suite *StatusCreateTestSuite) TestReplyToLocalStatus() {
- t := suite.testTokens["local_account_1"]
- oauthToken := oauth.DBTokenToToken(t)
-
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := testrig.CreateGinTestContext(recorder, nil)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
- ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
- ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
- ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", statuses.BasePath), nil) // the endpoint we're hitting
- ctx.Request.Header.Set("accept", "application/json")
- ctx.Request.Form = url.Values{
+ out, recorder := suite.postStatus(map[string][]string{
"status": {fmt.Sprintf("hello @%s this reply should work!", testrig.NewTestAccounts()["local_account_2"].Username)},
"in_reply_to_id": {testrig.NewTestStatuses()["local_account_2_status_1"].ID},
- }
- suite.statusModule.StatusCreatePOSTHandler(ctx)
-
- // check response
- suite.EqualValues(http.StatusOK, recorder.Code)
-
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- suite.NoError(err)
-
- statusReply := &apimodel.Status{}
- err = json.Unmarshal(b, statusReply)
- suite.NoError(err)
-
- suite.Equal("", statusReply.SpoilerText)
- suite.Equal(fmt.Sprintf("<p>hello <span class=\"h-card\"><a href=\"http://localhost:8080/@%s\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>%s</span></a></span> this reply should work!</p>", testrig.NewTestAccounts()["local_account_2"].Username, testrig.NewTestAccounts()["local_account_2"].Username), statusReply.Content)
- suite.False(statusReply.Sensitive)
- suite.Equal(apimodel.VisibilityPublic, statusReply.Visibility)
- suite.Equal(testrig.NewTestStatuses()["local_account_2_status_1"].ID, *statusReply.InReplyToID)
- suite.Equal(testrig.NewTestAccounts()["local_account_2"].ID, *statusReply.InReplyToAccountID)
- suite.Len(statusReply.Mentions, 1)
+ }, "")
+
+ // We should have OK from
+ // our call to the function.
+ suite.Equal(http.StatusOK, recorder.Code)
+
+ // in_reply_to_x
+ // fields should be set.
+ suite.Equal(`{
+ "account": "yeah this is my account, what about it punk",
+ "application": {
+ "name": "really cool gts application",
+ "website": "https://reallycool.app"
+ },
+ "bookmarked": false,
+ "card": null,
+ "content": "<p>hello <span class=\"h-card\"><a href=\"http://localhost:8080/@1happyturtle\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>1happyturtle</span></a></span> this reply should work!</p>",
+ "created_at": "right the hell just now babyee",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 0,
+ "id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
+ "in_reply_to_account_id": "01F8MH5NBDF2MV7CTC4Q5128HF",
+ "in_reply_to_id": "01F8MHBQCBTDKN6X5VHGMMN4MA",
+ "interaction_policy": {
+ "can_favourite": {
+ "always": [
+ "public",
+ "me"
+ ],
+ "with_approval": []
+ },
+ "can_reblog": {
+ "always": [
+ "public",
+ "me"
+ ],
+ "with_approval": []
+ },
+ "can_reply": {
+ "always": [
+ "public",
+ "me"
+ ],
+ "with_approval": []
+ }
+ },
+ "language": "en",
+ "media_attachments": [],
+ "mentions": [
+ {
+ "acct": "1happyturtle",
+ "id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
+ "url": "http://localhost:8080/@1happyturtle",
+ "username": "1happyturtle"
+ }
+ ],
+ "muted": false,
+ "pinned": false,
+ "poll": null,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "text": "hello @1happyturtle this reply should work!",
+ "uri": "http://localhost:8080/some/determinate/url",
+ "url": "http://localhost:8080/some/determinate/url",
+ "visibility": "public"
+}`, out)
}
-// Take a media file which is currently not associated with a status, and attach it to a new status.
+// Take a media file which is currently not associated
+// with a status, and attach it to a new status.
func (suite *StatusCreateTestSuite) TestAttachNewMediaSuccess() {
- t := suite.testTokens["local_account_1"]
- oauthToken := oauth.DBTokenToToken(t)
-
attachment := suite.testAttachments["local_account_1_unattached_1"]
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := testrig.CreateGinTestContext(recorder, nil)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
- ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
- ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
- ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", statuses.BasePath), nil) // the endpoint we're hitting
- ctx.Request.Header.Set("accept", "application/json")
- ctx.Request.Form = url.Values{
+ out, recorder := suite.postStatus(map[string][]string{
"status": {"here's an image attachment"},
"media_ids[]": {attachment.ID},
- }
- suite.statusModule.StatusCreatePOSTHandler(ctx)
-
- // check response
- suite.EqualValues(http.StatusOK, recorder.Code)
-
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- suite.NoError(err)
-
- statusResponse := &apimodel.Status{}
- err = json.Unmarshal(b, statusResponse)
- suite.NoError(err)
-
- suite.Equal("", statusResponse.SpoilerText)
- suite.Equal("<p>here's an image attachment</p>", statusResponse.Content)
- suite.False(statusResponse.Sensitive)
- suite.Equal(apimodel.VisibilityPublic, statusResponse.Visibility)
-
- // there should be one media attachment
- suite.Len(statusResponse.MediaAttachments, 1)
-
- // get the updated media attachment from the database
- gtsAttachment, err := suite.db.GetAttachmentByID(context.Background(), statusResponse.MediaAttachments[0].ID)
- suite.NoError(err)
-
- // convert it to a api attachment
- gtsAttachmentAsapi, err := suite.tc.AttachmentToAPIAttachment(context.Background(), gtsAttachment)
- suite.NoError(err)
-
- // compare it with what we have now
- suite.EqualValues(*statusResponse.MediaAttachments[0], gtsAttachmentAsapi)
-
- // the status id of the attachment should now be set to the id of the status we just created
- suite.Equal(statusResponse.ID, gtsAttachment.StatusID)
+ }, "")
+
+ // We should have OK from
+ // our call to the function.
+ suite.Equal(http.StatusOK, recorder.Code)
+
+ // Status should have
+ // media attached.
+ suite.Equal(`{
+ "account": "yeah this is my account, what about it punk",
+ "application": {
+ "name": "really cool gts application",
+ "website": "https://reallycool.app"
+ },
+ "bookmarked": false,
+ "card": null,
+ "content": "<p>here's an image attachment</p>",
+ "created_at": "right the hell just now babyee",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 0,
+ "id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "interaction_policy": {
+ "can_favourite": {
+ "always": [
+ "public",
+ "me"
+ ],
+ "with_approval": []
+ },
+ "can_reblog": {
+ "always": [
+ "public",
+ "me"
+ ],
+ "with_approval": []
+ },
+ "can_reply": {
+ "always": [
+ "public",
+ "me"
+ ],
+ "with_approval": []
+ }
+ },
+ "language": "en",
+ "media_attachments": [
+ {
+ "blurhash": "LNABP8o#Dge,S6M}axxVEQjYxWbH",
+ "description": "the oh you meme",
+ "id": "01F8MH8RMYQ6MSNY3JM2XT1CQ5",
+ "meta": {
+ "focus": {
+ "x": 0,
+ "y": 0
+ },
+ "original": {
+ "aspect": 1.7777778,
+ "height": 450,
+ "size": "800x450",
+ "width": 800
+ },
+ "small": {
+ "aspect": 1.7777778,
+ "height": 288,
+ "size": "512x288",
+ "width": 512
+ }
+ },
+ "preview_remote_url": null,
+ "preview_url": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01F8MH8RMYQ6MSNY3JM2XT1CQ5.webp",
+ "remote_url": null,
+ "text_url": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01F8MH8RMYQ6MSNY3JM2XT1CQ5.jpg",
+ "type": "image",
+ "url": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01F8MH8RMYQ6MSNY3JM2XT1CQ5.jpg"
+ }
+ ],
+ "mentions": [],
+ "muted": false,
+ "pinned": false,
+ "poll": null,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "text": "here's an image attachment",
+ "uri": "http://localhost:8080/some/determinate/url",
+ "url": "http://localhost:8080/some/determinate/url",
+ "visibility": "public"
+}`, out)
}
-// Post a new status with a language tag that is not in canonical format
+// Post a new status with a language
+// tag that is not in canonical format.
func (suite *StatusCreateTestSuite) TestPostNewStatusWithNoncanonicalLanguageTag() {
- t := suite.testTokens["local_account_1"]
- oauthToken := oauth.DBTokenToToken(t)
-
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := testrig.CreateGinTestContext(recorder, nil)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
- ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
- ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
- ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", statuses.BasePath), nil) // the endpoint we're hitting
- ctx.Request.Header.Set("accept", "application/json")
- ctx.Request.Form = url.Values{
+ out, recorder := suite.postStatus(map[string][]string{
"status": {"English? what's English? i speak American"},
"language": {"en-us"},
- }
- suite.statusModule.StatusCreatePOSTHandler(ctx)
-
- suite.EqualValues(http.StatusOK, recorder.Code)
-
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- suite.NoError(err)
-
- statusReply := &apimodel.Status{}
- err = json.Unmarshal(b, statusReply)
- suite.NoError(err)
-
- suite.Equal("<p>English? what's English? i speak American</p>", statusReply.Content)
- suite.NotNil(statusReply.Language)
- suite.Equal("en-US", *statusReply.Language)
-}
-
-// Post a new status with an attached poll.
-func (suite *StatusCreateTestSuite) testPostNewStatusWithPoll(configure func(request *http.Request)) {
- t := suite.testTokens["local_account_1"]
- oauthToken := oauth.DBTokenToToken(t)
-
- // setup
- recorder := httptest.NewRecorder()
- ctx, _ := testrig.CreateGinTestContext(recorder, nil)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
- ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
- ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
- ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", statuses.BasePath), nil) // the endpoint we're hitting
- ctx.Request.Header.Set("accept", "application/json")
- configure(ctx.Request)
- suite.statusModule.StatusCreatePOSTHandler(ctx)
-
- suite.EqualValues(http.StatusOK, recorder.Code)
-
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- suite.NoError(err)
-
- statusReply := &apimodel.Status{}
- err = json.Unmarshal(b, statusReply)
- suite.NoError(err)
-
- suite.Equal("<p>this is a status with a poll!</p>", statusReply.Content)
- suite.Equal(apimodel.VisibilityPublic, statusReply.Visibility)
- if suite.NotNil(statusReply.Poll) {
- if suite.Len(statusReply.Poll.Options, 2) {
- suite.Equal("first option", statusReply.Poll.Options[0].Title)
- suite.Equal("second option", statusReply.Poll.Options[1].Title)
- }
- suite.NotZero(statusReply.Poll.ExpiresAt)
- suite.False(statusReply.Poll.Expired)
- suite.True(statusReply.Poll.Multiple)
- }
+ }, "")
+
+ // We should have OK from
+ // our call to the function.
+ suite.Equal(http.StatusOK, recorder.Code)
+
+ // The returned language tag should
+ // use its canonicalized version rather
+ // than the format we submitted.
+ suite.Equal(`{
+ "account": "yeah this is my account, what about it punk",
+ "application": {
+ "name": "really cool gts application",
+ "website": "https://reallycool.app"
+ },
+ "bookmarked": false,
+ "card": null,
+ "content": "<p>English? what's English? i speak American</p>",
+ "created_at": "right the hell just now babyee",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 0,
+ "id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "interaction_policy": {
+ "can_favourite": {
+ "always": [
+ "public",
+ "me"
+ ],
+ "with_approval": []
+ },
+ "can_reblog": {
+ "always": [
+ "public",
+ "me"
+ ],
+ "with_approval": []
+ },
+ "can_reply": {
+ "always": [
+ "public",
+ "me"
+ ],
+ "with_approval": []
+ }
+ },
+ "language": "en-US",
+ "media_attachments": [],
+ "mentions": [],
+ "muted": false,
+ "pinned": false,
+ "poll": null,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "text": "English? what's English? i speak American",
+ "uri": "http://localhost:8080/some/determinate/url",
+ "url": "http://localhost:8080/some/determinate/url",
+ "visibility": "public"
+}`, out)
}
func (suite *StatusCreateTestSuite) TestPostNewStatusWithPollForm() {
- suite.testPostNewStatusWithPoll(func(request *http.Request) {
- request.Form = url.Values{
- "status": {"this is a status with a poll!"},
- "visibility": {"public"},
- "poll[options][]": {"first option", "second option"},
- "poll[expires_in]": {"3600"},
- "poll[multiple]": {"true"},
- }
- })
+ out, recorder := suite.postStatus(map[string][]string{
+ "status": {"this is a status with a poll!"},
+ "visibility": {"public"},
+ "poll[options][]": {"first option", "second option"},
+ "poll[expires_in]": {"3600"},
+ "poll[multiple]": {"true"},
+ }, "")
+
+ // We should have OK from
+ // our call to the function.
+ suite.Equal(http.StatusOK, recorder.Code)
+
+ // Status poll should
+ // be as expected.
+ suite.Equal(`{
+ "account": "yeah this is my account, what about it punk",
+ "application": {
+ "name": "really cool gts application",
+ "website": "https://reallycool.app"
+ },
+ "bookmarked": false,
+ "card": null,
+ "content": "<p>this is a status with a poll!</p>",
+ "created_at": "right the hell just now babyee",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 0,
+ "id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "interaction_policy": {
+ "can_favourite": {
+ "always": [
+ "public",
+ "me"
+ ],
+ "with_approval": []
+ },
+ "can_reblog": {
+ "always": [
+ "public",
+ "me"
+ ],
+ "with_approval": []
+ },
+ "can_reply": {
+ "always": [
+ "public",
+ "me"
+ ],
+ "with_approval": []
+ }
+ },
+ "language": "en",
+ "media_attachments": [],
+ "mentions": [],
+ "muted": false,
+ "pinned": false,
+ "poll": {
+ "emojis": [],
+ "expired": false,
+ "expires_at": "ah like you know whatever dude it's chill",
+ "id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
+ "multiple": true,
+ "options": [
+ {
+ "title": "first option",
+ "votes_count": 0
+ },
+ {
+ "title": "second option",
+ "votes_count": 0
+ }
+ ],
+ "own_votes": [],
+ "voted": true,
+ "voters_count": 0,
+ "votes_count": 0
+ },
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "text": "this is a status with a poll!",
+ "uri": "http://localhost:8080/some/determinate/url",
+ "url": "http://localhost:8080/some/determinate/url",
+ "visibility": "public"
+}`, out)
}
func (suite *StatusCreateTestSuite) TestPostNewStatusWithPollJSON() {
- suite.testPostNewStatusWithPoll(func(request *http.Request) {
- request.Header.Set("content-type", "application/json")
- request.Body = io.NopCloser(strings.NewReader(`{
- "status": "this is a status with a poll!",
- "visibility": "public",
- "poll": {
- "options": ["first option", "second option"],
- "expires_in": 3600,
- "multiple": true
- }
- }`))
- })
+ out, recorder := suite.postStatus(nil, `{
+ "status": "this is a status with a poll!",
+ "visibility": "public",
+ "poll": {
+ "options": ["first option", "second option"],
+ "expires_in": 3600,
+ "multiple": true
+ }
+}`)
+
+ // We should have OK from
+ // our call to the function.
+ suite.Equal(http.StatusOK, recorder.Code)
+
+ // Status poll should
+ // be as expected.
+ suite.Equal(`{
+ "account": "yeah this is my account, what about it punk",
+ "application": {
+ "name": "really cool gts application",
+ "website": "https://reallycool.app"
+ },
+ "bookmarked": false,
+ "card": null,
+ "content": "<p>this is a status with a poll!</p>",
+ "created_at": "right the hell just now babyee",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 0,
+ "id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "interaction_policy": {
+ "can_favourite": {
+ "always": [
+ "public",
+ "me"
+ ],
+ "with_approval": []
+ },
+ "can_reblog": {
+ "always": [
+ "public",
+ "me"
+ ],
+ "with_approval": []
+ },
+ "can_reply": {
+ "always": [
+ "public",
+ "me"
+ ],
+ "with_approval": []
+ }
+ },
+ "language": "en",
+ "media_attachments": [],
+ "mentions": [],
+ "muted": false,
+ "pinned": false,
+ "poll": {
+ "emojis": [],
+ "expired": false,
+ "expires_at": "ah like you know whatever dude it's chill",
+ "id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
+ "multiple": true,
+ "options": [
+ {
+ "title": "first option",
+ "votes_count": 0
+ },
+ {
+ "title": "second option",
+ "votes_count": 0
+ }
+ ],
+ "own_votes": [],
+ "voted": true,
+ "voters_count": 0,
+ "votes_count": 0
+ },
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "text": "this is a status with a poll!",
+ "uri": "http://localhost:8080/some/determinate/url",
+ "url": "http://localhost:8080/some/determinate/url",
+ "visibility": "public"
+}`, out)
}
func TestStatusCreateTestSuite(t *testing.T) {
diff --git a/internal/api/model/status.go b/internal/api/model/status.go
index 9b83fa582..c29ab3e82 100644
--- a/internal/api/model/status.go
+++ b/internal/api/model/status.go
@@ -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.
diff --git a/internal/processing/status/create.go b/internal/processing/status/create.go
index 5f2cb8212..1513018ae 100644
--- a/internal/processing/status/create.go
+++ b/internal/processing/status/create.go
@@ -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 {