diff --git a/internal/apimodule/status/statuscreate.go b/internal/apimodule/status/statuscreate.go index fd20280..0981caa 100644 --- a/internal/apimodule/status/statuscreate.go +++ b/internal/apimodule/status/statuscreate.go @@ -27,6 +27,7 @@ import ( "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db/model" "github.com/superseriousbusiness/gotosocial/internal/distributor" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -36,12 +37,12 @@ import ( type advancedStatusCreateForm struct { mastotypes.StatusCreateRequest - AdvancedVisibility *advancedVisibilityFlagsForm `form:"visibility_advanced"` + advancedVisibilityFlagsForm } type advancedVisibilityFlagsForm struct { // The gotosocial visibility model - Visibility *model.Visibility + VisibilityAdvanced *model.Visibility `form:"visibility_advanced"` // This status will be federated beyond the local timeline(s) Federated *bool `form:"federated"` // This status can be boosted/reblogged @@ -70,7 +71,7 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) { } // extract the status create form from the request context - l.Trace("parsing request form") + l.Tracef("parsing request form: %s", c.Request.Form) form := &advancedStatusCreateForm{} if err := c.ShouldBind(form); err != nil || form == nil { l.Debugf("could not parse form from request: %s", err) @@ -128,6 +129,12 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) { return } + // handle language settings + if err := parseLanguage(form, authed.Account.Language, newStatus); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + // convert mentions to *model.Mention menchies, err := m.db.MentionStringsToMentions(util.DeriveMentions(form.Status), authed.Account.ID, thisStatusID) if err != nil { @@ -186,8 +193,9 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) { URI: newStatus.URI, URL: newStatus.URL, Content: newStatus.Content, - Application: authed.Application.ToMasto(), + Application: authed.Application.ToMastoPublic(), Account: mastoAccount, + // MediaAttachments: , Text: form.Status, } c.JSON(http.StatusOK, mastoStatus) @@ -260,8 +268,8 @@ func parseVisibility(form *advancedStatusCreateForm, accountDefaultVis model.Vis // Advanced takes priority if it's set. // If it's not set, take whatever masto visibility is set. // If *that's* not set either, then just take the account default. - if form.AdvancedVisibility != nil && form.AdvancedVisibility.Visibility != nil { - gtsBasicVis = *form.AdvancedVisibility.Visibility + if form.VisibilityAdvanced != nil { + gtsBasicVis = *form.VisibilityAdvanced } else if form.Visibility != "" { gtsBasicVis = util.ParseGTSVisFromMastoVis(form.Visibility) } else { @@ -274,40 +282,38 @@ func parseVisibility(form *advancedStatusCreateForm, accountDefaultVis model.Vis break case model.VisibilityUnlocked: // for unlocked the user can set any combination of flags they like so look at them all to see if they're set and then apply them - if form.AdvancedVisibility != nil { - if form.AdvancedVisibility.Federated != nil { - gtsAdvancedVis.Federated = *form.AdvancedVisibility.Federated - } - - if form.AdvancedVisibility.Boostable != nil { - gtsAdvancedVis.Boostable = *form.AdvancedVisibility.Boostable - } - - if form.AdvancedVisibility.Replyable != nil { - gtsAdvancedVis.Replyable = *form.AdvancedVisibility.Replyable - } - - if form.AdvancedVisibility.Likeable != nil { - gtsAdvancedVis.Likeable = *form.AdvancedVisibility.Likeable - } + if form.Federated != nil { + gtsAdvancedVis.Federated = *form.Federated } + + if form.Boostable != nil { + gtsAdvancedVis.Boostable = *form.Boostable + } + + if form.Replyable != nil { + gtsAdvancedVis.Replyable = *form.Replyable + } + + if form.Likeable != nil { + gtsAdvancedVis.Likeable = *form.Likeable + } + case model.VisibilityFollowersOnly, model.VisibilityMutualsOnly: // for followers or mutuals only, boostable will *always* be false, but the other fields can be set so check and apply them gtsAdvancedVis.Boostable = false - if form.AdvancedVisibility != nil { - if form.AdvancedVisibility.Federated != nil { - gtsAdvancedVis.Federated = *form.AdvancedVisibility.Federated - } - - if form.AdvancedVisibility.Replyable != nil { - gtsAdvancedVis.Replyable = *form.AdvancedVisibility.Replyable - } - - if form.AdvancedVisibility.Likeable != nil { - gtsAdvancedVis.Likeable = *form.AdvancedVisibility.Likeable - } + if form.Federated != nil { + gtsAdvancedVis.Federated = *form.Federated } + + if form.Replyable != nil { + gtsAdvancedVis.Replyable = *form.Replyable + } + + if form.Likeable != nil { + gtsAdvancedVis.Likeable = *form.Likeable + } + case model.VisibilityDirect: // direct is pretty easy: there's only one possible setting so return it gtsAdvancedVis.Federated = true @@ -336,9 +342,18 @@ func (m *statusModule) parseReplyToID(form *advancedStatusCreateForm, thisAccoun repliedStatus := &model.Status{} repliedAccount := &model.Account{} // check replied status exists + is replyable - if err := m.db.GetByID(form.InReplyToID, repliedStatus); err != nil || !repliedStatus.VisibilityAdvanced.Replyable { - return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err) + if err := m.db.GetByID(form.InReplyToID, repliedStatus); err != nil { + if _, ok := err.(db.ErrNoEntries); ok { + return fmt.Errorf("status with id %s not replyable because it doesn't exist", form.InReplyToID) + } else { + return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err) + } } + + if !repliedStatus.VisibilityAdvanced.Replyable { + return fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID) + } + // check replied account is known to us if err := m.db.GetByID(repliedStatus.AccountID, repliedAccount); err != nil { return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err) @@ -373,3 +388,15 @@ func (m *statusModule) parseMediaIDs(form *advancedStatusCreateForm, thisAccount status.Attachments = attachments return nil } + +func parseLanguage(form *advancedStatusCreateForm, accountDefaultLanguage string, status *model.Status) error { + if form.Language != "" { + status.Language = form.Language + } else { + status.Language = accountDefaultLanguage + } + if status.Language == "" { + return errors.New("no language given either in status create form or account default") + } + return nil +} diff --git a/internal/apimodule/status/statuscreate_test.go b/internal/apimodule/status/statuscreate_test.go index e447f8b..6a6aa9e 100644 --- a/internal/apimodule/status/statuscreate_test.go +++ b/internal/apimodule/status/statuscreate_test.go @@ -165,27 +165,13 @@ func (suite *StatusCreateTestSuite) TestStatusCreatePOSTHandlerSuccessful() { ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", basePath), nil) // the endpoint we're hitting ctx.Request.Form = url.Values{ - "status": {"this is a brand new status!"}, - "spoiler_text": {"hello hello"}, - "sensitive": {"true"}, - "visibility": {"public"}, - // Status string `form:"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"` - // // Poll to include with this status. - // Poll *PollRequest `form:"poll"` - // // ID of the status being replied to, if status is a reply - // InReplyToID string `form:"in_reply_to_id"` - // // Mark status and attached media as sensitive? - // Sensitive bool `form:"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"` - // // Visibility of the posted status. Enumerable oneOf public, unlisted, private, direct. - // Visibility Visibility `form:"visibility"` - // // ISO 8601 Datetime at which to schedule a status. Providing this paramter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future. - // ScheduledAt string `form:"scheduled_at"` - // // ISO 639 language code for this status. - // Language string `form:"language"` + "status": {"this is a brand new status!"}, + "spoiler_text": {"hello hello"}, + "sensitive": {"true"}, + "visibility_advanced": {"mutuals_only"}, + "likeable": {"false"}, + "replyable": {"false"}, + "federated": {"false"}, } suite.statusModule.statusCreatePOSTHandler(ctx) @@ -198,6 +184,7 @@ func (suite *StatusCreateTestSuite) TestStatusCreatePOSTHandlerSuccessful() { defer result.Body.Close() b, err := ioutil.ReadAll(result.Body) assert.NoError(suite.T(), err) + fmt.Println(string(b)) statusReply := &mastotypes.Status{} err = json.Unmarshal(b, statusReply) @@ -206,7 +193,47 @@ func (suite *StatusCreateTestSuite) TestStatusCreatePOSTHandlerSuccessful() { assert.Equal(suite.T(), "hello hello", statusReply.SpoilerText) assert.Equal(suite.T(), "this is a brand new status!", statusReply.Content) assert.True(suite.T(), statusReply.Sensitive) - assert.Equal(suite.T(), mastotypes.VisibilityPublic, statusReply.Visibility) + assert.Equal(suite.T(), mastotypes.VisibilityPrivate, statusReply.Visibility) +} + +func (suite *StatusCreateTestSuite) TestStatusCreatePOSTHandlerReplyToFail() { + t := suite.testTokens["local_account_1"] + oauthToken := oauth.PGTokenToOauthToken(t) + + // setup + recorder := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(recorder) + 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", basePath), nil) // the endpoint we're hitting + ctx.Request.Form = url.Values{ + "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 + + // 1. we should have OK from our call to the function + suite.EqualValues(http.StatusOK, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + b, err := ioutil.ReadAll(result.Body) + assert.NoError(suite.T(), err) + fmt.Println(string(b)) + + statusReply := &mastotypes.Status{} + err = json.Unmarshal(b, statusReply) + assert.NoError(suite.T(), err) + + assert.Equal(suite.T(), "hello hello", statusReply.SpoilerText) + assert.Equal(suite.T(), "this is a brand new status!", statusReply.Content) + assert.True(suite.T(), statusReply.Sensitive) + assert.Equal(suite.T(), mastotypes.VisibilityPrivate, statusReply.Visibility) } func TestStatusCreateTestSuite(t *testing.T) { diff --git a/internal/util/parse.go b/internal/util/parse.go index 525c551..92baac6 100644 --- a/internal/util/parse.go +++ b/internal/util/parse.go @@ -63,12 +63,36 @@ func GenerateURIs(username string, protocol string, host string) *URIs { } } +// ParseGTSVisFromMastoVis converts a mastodon visibility into its gts equivalent. func ParseGTSVisFromMastoVis(m mastotypes.Visibility) model.Visibility { - // TODO: convert a masto vis into a gts vis + switch m { + case mastotypes.VisibilityPublic: + return model.VisibilityPublic + case mastotypes.VisibilityUnlisted: + return model.VisibilityUnlocked + case mastotypes.VisibilityPrivate: + return model.VisibilityFollowersOnly + case mastotypes.VisibilityDirect: + return model.VisibilityDirect + default: + break + } return "" } +// ParseMastoVisFromGTSVis converts a gts visibility into its mastodon equivalent func ParseMastoVisFromGTSVis(m model.Visibility) mastotypes.Visibility { - // TODO: convert a gts vis into a masto vis + switch m { + case model.VisibilityPublic: + return mastotypes.VisibilityPublic + case model.VisibilityUnlocked: + return mastotypes.VisibilityUnlisted + case model.VisibilityFollowersOnly, model.VisibilityMutualsOnly: + return mastotypes.VisibilityPrivate + case model.VisibilityDirect: + return mastotypes.VisibilityDirect + default: + break + } return "" }