mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2024-09-29 18:44:27 +00:00
Merge 81256a6ff7
into 77b095a8c3
This commit is contained in:
commit
d811be24b7
|
@ -18,6 +18,12 @@
|
|||
package statuses_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/statuses"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
|
@ -25,6 +31,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
|
@ -59,6 +66,113 @@ type StatusStandardTestSuite struct {
|
|||
statusModule *statuses.Module
|
||||
}
|
||||
|
||||
// Normalizes a status response to a determinate
|
||||
// form, and pretty-prints it to JSON.
|
||||
func (suite *StatusStandardTestSuite) parseStatusResponse(
|
||||
recorder *httptest.ResponseRecorder,
|
||||
) (string, *httptest.ResponseRecorder) {
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
rawMap := make(map[string]any)
|
||||
if err := json.Unmarshal(data, &rawMap); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Make status fields determinate.
|
||||
suite.determinateStatus(rawMap)
|
||||
|
||||
// For readability, don't
|
||||
// escape HTML, and indent json.
|
||||
out := new(bytes.Buffer)
|
||||
enc := json.NewEncoder(out)
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.SetIndent("", " ")
|
||||
|
||||
if err := enc.Encode(&rawMap); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
return strings.TrimSpace(out.String()), recorder
|
||||
}
|
||||
|
||||
func (suite *StatusStandardTestSuite) determinateStatus(rawMap map[string]any) {
|
||||
// Replace any fields from the raw map that
|
||||
// aren't determinate (date, id, url, etc).
|
||||
if _, ok := rawMap["id"]; ok {
|
||||
rawMap["id"] = id.Highest
|
||||
}
|
||||
|
||||
if _, ok := rawMap["uri"]; ok {
|
||||
rawMap["uri"] = "http://localhost:8080/some/determinate/url"
|
||||
}
|
||||
|
||||
if _, ok := rawMap["url"]; ok {
|
||||
rawMap["url"] = "http://localhost:8080/some/determinate/url"
|
||||
}
|
||||
|
||||
if _, ok := rawMap["created_at"]; ok {
|
||||
rawMap["created_at"] = "right the hell just now babyee"
|
||||
}
|
||||
|
||||
// Make ID of any mentions determinate.
|
||||
if menchiesRaw, ok := rawMap["mentions"]; ok {
|
||||
menchies, ok := menchiesRaw.([]any)
|
||||
if !ok {
|
||||
suite.FailNow("couldn't coerce menchies")
|
||||
}
|
||||
|
||||
for _, menchieRaw := range menchies {
|
||||
menchie, ok := menchieRaw.(map[string]any)
|
||||
if !ok {
|
||||
suite.FailNow("couldn't coerce menchie")
|
||||
}
|
||||
|
||||
if _, ok := menchie["id"]; ok {
|
||||
menchie["id"] = id.Highest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
if _, ok := poll["id"]; ok {
|
||||
poll["id"] = id.Highest
|
||||
}
|
||||
|
||||
if _, ok := poll["expires_at"]; ok {
|
||||
poll["expires_at"] = "ah like you know whatever dude it's chill"
|
||||
}
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
||||
|
||||
// If status contains an embedded
|
||||
// reblog do the same thing for that.
|
||||
if reblogRaw, ok := rawMap["reblog"]; ok && reblogRaw != nil {
|
||||
reblog, ok := reblogRaw.(map[string]any)
|
||||
if !ok {
|
||||
suite.FailNow("couldn't coerce reblog")
|
||||
}
|
||||
suite.determinateStatus(reblog)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *StatusStandardTestSuite) SetupSuite() {
|
||||
suite.testTokens = testrig.NewTestTokens()
|
||||
suite.testClients = testrig.NewTestClients()
|
||||
|
|
|
@ -17,9 +17,6 @@ package statuses_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
|
@ -28,7 +25,7 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/statuses"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
|
@ -38,212 +35,596 @@ type StatusBoostTestSuite struct {
|
|||
StatusStandardTestSuite
|
||||
}
|
||||
|
||||
func (suite *StatusBoostTestSuite) TestPostBoost() {
|
||||
t := suite.testTokens["local_account_1"]
|
||||
oauthToken := oauth.DBTokenToToken(t)
|
||||
|
||||
targetStatus := suite.testStatuses["admin_account_status_1"]
|
||||
|
||||
// setup
|
||||
func (suite *StatusBoostTestSuite) postStatusBoost(
|
||||
targetStatusID string,
|
||||
app *gtsmodel.Application,
|
||||
token *gtsmodel.Token,
|
||||
user *gtsmodel.User,
|
||||
account *gtsmodel.Account,
|
||||
) (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.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", strings.Replace(statuses.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting
|
||||
ctx.Set(oauth.SessionAuthorizedApplication, app)
|
||||
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(token))
|
||||
ctx.Set(oauth.SessionAuthorizedUser, user)
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, account)
|
||||
|
||||
const pathBase = "http://localhost:8080/api" + statuses.ReblogPath
|
||||
path := strings.ReplaceAll(pathBase, ":"+apiutil.IDKey, targetStatusID)
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, path, nil)
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
|
||||
// normally the router would populate these params from the path values,
|
||||
// but because we're calling the function directly, we need to set them manually.
|
||||
// Populate target status ID.
|
||||
ctx.Params = gin.Params{
|
||||
gin.Param{
|
||||
Key: statuses.IDKey,
|
||||
Value: targetStatus.ID,
|
||||
Key: apiutil.IDKey,
|
||||
Value: targetStatusID,
|
||||
},
|
||||
}
|
||||
|
||||
// Trigger handler.
|
||||
suite.statusModule.StatusBoostPOSTHandler(ctx)
|
||||
return suite.parseStatusResponse(recorder)
|
||||
}
|
||||
|
||||
// check response
|
||||
suite.EqualValues(http.StatusOK, recorder.Code)
|
||||
func (suite *StatusBoostTestSuite) TestPostBoost() {
|
||||
var (
|
||||
targetStatus = suite.testStatuses["admin_account_status_1"]
|
||||
app = suite.testApplications["application_1"]
|
||||
token = suite.testTokens["local_account_1"]
|
||||
user = suite.testUsers["local_account_1"]
|
||||
account = suite.testAccounts["local_account_1"]
|
||||
)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
out, recorder := suite.postStatusBoost(
|
||||
targetStatus.ID,
|
||||
app,
|
||||
token,
|
||||
user,
|
||||
account,
|
||||
)
|
||||
|
||||
statusReply := &apimodel.Status{}
|
||||
err = json.Unmarshal(b, statusReply)
|
||||
suite.NoError(err)
|
||||
// We should have OK from
|
||||
// our call to the function.
|
||||
suite.Equal(http.StatusOK, recorder.Code)
|
||||
|
||||
suite.False(statusReply.Sensitive)
|
||||
suite.Equal(apimodel.VisibilityPublic, statusReply.Visibility)
|
||||
|
||||
suite.Empty(statusReply.SpoilerText)
|
||||
suite.Empty(statusReply.Content)
|
||||
suite.Equal("the_mighty_zork", statusReply.Account.Username)
|
||||
suite.Len(statusReply.MediaAttachments, 0)
|
||||
suite.Len(statusReply.Mentions, 0)
|
||||
suite.Len(statusReply.Emojis, 0)
|
||||
suite.Len(statusReply.Tags, 0)
|
||||
|
||||
suite.NotNil(statusReply.Application)
|
||||
suite.Equal("really cool gts application", statusReply.Application.Name)
|
||||
|
||||
suite.NotNil(statusReply.Reblog)
|
||||
suite.Equal(1, statusReply.Reblog.ReblogsCount)
|
||||
suite.Equal(1, statusReply.Reblog.FavouritesCount)
|
||||
suite.Equal(targetStatus.Content, statusReply.Reblog.Content)
|
||||
suite.Equal(targetStatus.ContentWarning, statusReply.Reblog.SpoilerText)
|
||||
suite.Equal(targetStatus.AccountID, statusReply.Reblog.Account.ID)
|
||||
suite.Len(statusReply.Reblog.MediaAttachments, 1)
|
||||
suite.Len(statusReply.Reblog.Tags, 1)
|
||||
suite.Len(statusReply.Reblog.Emojis, 1)
|
||||
suite.True(statusReply.Reblogged)
|
||||
suite.True(statusReply.Reblog.Reblogged)
|
||||
suite.Equal("superseriousbusiness", statusReply.Reblog.Application.Name)
|
||||
// Target status should now
|
||||
// be "reblogged" by us.
|
||||
suite.Equal(`{
|
||||
"account": "yeah this is my account, what about it punk",
|
||||
"application": {
|
||||
"name": "really cool gts application",
|
||||
"website": "https://reallycool.app"
|
||||
},
|
||||
"bookmarked": true,
|
||||
"card": null,
|
||||
"content": "",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"emojis": [],
|
||||
"favourited": true,
|
||||
"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": null,
|
||||
"media_attachments": [],
|
||||
"mentions": [],
|
||||
"muted": false,
|
||||
"pinned": false,
|
||||
"poll": null,
|
||||
"reblog": {
|
||||
"account": "yeah this is my account, what about it punk",
|
||||
"application": {
|
||||
"name": "superseriousbusiness",
|
||||
"website": "https://superserious.business"
|
||||
},
|
||||
"bookmarked": true,
|
||||
"card": null,
|
||||
"content": "hello world! #welcome ! first post on the instance :rainbow: !",
|
||||
"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": true,
|
||||
"favourites_count": 1,
|
||||
"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": "LIIE|gRj00WB-;j[t7j[4nWBj[Rj",
|
||||
"description": "Black and white image of some 50's style text saying: Welcome On Board",
|
||||
"id": "01F8MH6NEM8D7527KZAECTCR76",
|
||||
"meta": {
|
||||
"focus": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"original": {
|
||||
"aspect": 1.9047619,
|
||||
"height": 630,
|
||||
"size": "1200x630",
|
||||
"width": 1200
|
||||
},
|
||||
"small": {
|
||||
"aspect": 1.9104477,
|
||||
"height": 268,
|
||||
"size": "512x268",
|
||||
"width": 512
|
||||
}
|
||||
},
|
||||
"preview_remote_url": null,
|
||||
"preview_url": "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/small/01F8MH6NEM8D7527KZAECTCR76.webp",
|
||||
"remote_url": null,
|
||||
"text_url": "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg",
|
||||
"type": "image",
|
||||
"url": "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg"
|
||||
}
|
||||
],
|
||||
"mentions": [],
|
||||
"muted": false,
|
||||
"pinned": false,
|
||||
"poll": null,
|
||||
"reblog": null,
|
||||
"reblogged": true,
|
||||
"reblogs_count": 1,
|
||||
"replies_count": 1,
|
||||
"sensitive": false,
|
||||
"spoiler_text": "",
|
||||
"tags": [
|
||||
{
|
||||
"name": "welcome",
|
||||
"url": "http://localhost:8080/tags/welcome"
|
||||
}
|
||||
],
|
||||
"text": "hello world! #welcome ! first post on the instance :rainbow: !",
|
||||
"uri": "http://localhost:8080/some/determinate/url",
|
||||
"url": "http://localhost:8080/some/determinate/url",
|
||||
"visibility": "public"
|
||||
},
|
||||
"reblogged": true,
|
||||
"reblogs_count": 0,
|
||||
"replies_count": 0,
|
||||
"sensitive": false,
|
||||
"spoiler_text": "",
|
||||
"tags": [],
|
||||
"uri": "http://localhost:8080/some/determinate/url",
|
||||
"url": "http://localhost:8080/some/determinate/url",
|
||||
"visibility": "public"
|
||||
}`, out)
|
||||
}
|
||||
|
||||
func (suite *StatusBoostTestSuite) TestPostBoostOwnFollowersOnly() {
|
||||
t := suite.testTokens["local_account_1"]
|
||||
oauthToken := oauth.DBTokenToToken(t)
|
||||
var (
|
||||
targetStatus = suite.testStatuses["local_account_1_status_5"]
|
||||
app = suite.testApplications["application_1"]
|
||||
token = suite.testTokens["local_account_1"]
|
||||
user = suite.testUsers["local_account_1"]
|
||||
account = suite.testAccounts["local_account_1"]
|
||||
)
|
||||
|
||||
testStatus := suite.testStatuses["local_account_1_status_5"]
|
||||
testAccount := suite.testAccounts["local_account_1"]
|
||||
testUser := suite.testUsers["local_account_1"]
|
||||
out, recorder := suite.postStatusBoost(
|
||||
targetStatus.ID,
|
||||
app,
|
||||
token,
|
||||
user,
|
||||
account,
|
||||
)
|
||||
|
||||
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, testUser)
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, testAccount)
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.ReblogPath, ":id", testStatus.ID, 1)), nil)
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
// We should have OK from
|
||||
// our call to the function.
|
||||
suite.Equal(http.StatusOK, recorder.Code)
|
||||
|
||||
ctx.Params = gin.Params{
|
||||
gin.Param{
|
||||
Key: statuses.IDKey,
|
||||
Value: testStatus.ID,
|
||||
},
|
||||
}
|
||||
|
||||
suite.statusModule.StatusBoostPOSTHandler(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)
|
||||
|
||||
responseStatus := &apimodel.Status{}
|
||||
err = json.Unmarshal(b, responseStatus)
|
||||
suite.NoError(err)
|
||||
|
||||
suite.False(responseStatus.Sensitive)
|
||||
suite.Equal(suite.tc.VisToAPIVis(context.Background(), testStatus.Visibility), responseStatus.Visibility)
|
||||
|
||||
suite.Empty(responseStatus.SpoilerText)
|
||||
suite.Empty(responseStatus.Content)
|
||||
suite.Equal("the_mighty_zork", responseStatus.Account.Username)
|
||||
suite.Len(responseStatus.MediaAttachments, 0)
|
||||
suite.Len(responseStatus.Mentions, 0)
|
||||
suite.Len(responseStatus.Emojis, 0)
|
||||
suite.Len(responseStatus.Tags, 0)
|
||||
|
||||
suite.NotNil(responseStatus.Application)
|
||||
suite.Equal("really cool gts application", responseStatus.Application.Name)
|
||||
|
||||
suite.NotNil(responseStatus.Reblog)
|
||||
suite.Equal(1, responseStatus.Reblog.ReblogsCount)
|
||||
suite.Equal(0, responseStatus.Reblog.FavouritesCount)
|
||||
suite.Equal(testStatus.Content, responseStatus.Reblog.Content)
|
||||
suite.Equal(testStatus.ContentWarning, responseStatus.Reblog.SpoilerText)
|
||||
suite.Equal(testStatus.AccountID, responseStatus.Reblog.Account.ID)
|
||||
suite.Equal(suite.tc.VisToAPIVis(context.Background(), testStatus.Visibility), responseStatus.Reblog.Visibility)
|
||||
suite.Empty(responseStatus.Reblog.MediaAttachments)
|
||||
suite.Empty(responseStatus.Reblog.Tags)
|
||||
suite.Empty(responseStatus.Reblog.Emojis)
|
||||
suite.True(responseStatus.Reblogged)
|
||||
suite.True(responseStatus.Reblog.Reblogged)
|
||||
suite.Equal("really cool gts application", responseStatus.Reblog.Application.Name)
|
||||
// Target status should now
|
||||
// be "reblogged" by us.
|
||||
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": "",
|
||||
"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": null,
|
||||
"media_attachments": [],
|
||||
"mentions": [],
|
||||
"muted": false,
|
||||
"pinned": false,
|
||||
"poll": null,
|
||||
"reblog": {
|
||||
"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": "hi!",
|
||||
"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": true,
|
||||
"reblogs_count": 1,
|
||||
"replies_count": 0,
|
||||
"sensitive": false,
|
||||
"spoiler_text": "",
|
||||
"tags": [],
|
||||
"text": "hi!",
|
||||
"uri": "http://localhost:8080/some/determinate/url",
|
||||
"url": "http://localhost:8080/some/determinate/url",
|
||||
"visibility": "private"
|
||||
},
|
||||
"reblogged": true,
|
||||
"reblogs_count": 0,
|
||||
"replies_count": 0,
|
||||
"sensitive": false,
|
||||
"spoiler_text": "",
|
||||
"tags": [],
|
||||
"uri": "http://localhost:8080/some/determinate/url",
|
||||
"url": "http://localhost:8080/some/determinate/url",
|
||||
"visibility": "private"
|
||||
}`, out)
|
||||
}
|
||||
|
||||
// try to boost a status that's not boostable / visible to us
|
||||
// Try to boost a status that's
|
||||
// not boostable / visible to us.
|
||||
func (suite *StatusBoostTestSuite) TestPostUnboostable() {
|
||||
t := suite.testTokens["local_account_1"]
|
||||
oauthToken := oauth.DBTokenToToken(t)
|
||||
var (
|
||||
targetStatus = suite.testStatuses["local_account_2_status_4"]
|
||||
app = suite.testApplications["application_1"]
|
||||
token = suite.testTokens["local_account_1"]
|
||||
user = suite.testUsers["local_account_1"]
|
||||
account = suite.testAccounts["local_account_1"]
|
||||
)
|
||||
|
||||
targetStatus := suite.testStatuses["local_account_2_status_4"]
|
||||
out, recorder := suite.postStatusBoost(
|
||||
targetStatus.ID,
|
||||
app,
|
||||
token,
|
||||
user,
|
||||
account,
|
||||
)
|
||||
|
||||
// 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", strings.Replace(statuses.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
|
||||
// normally the router would populate these params from the path values,
|
||||
// but because we're calling the function directly, we need to set them manually.
|
||||
ctx.Params = gin.Params{
|
||||
gin.Param{
|
||||
Key: statuses.IDKey,
|
||||
Value: targetStatus.ID,
|
||||
},
|
||||
}
|
||||
|
||||
suite.statusModule.StatusBoostPOSTHandler(ctx)
|
||||
|
||||
// check response
|
||||
// We should have 403 from
|
||||
// our call to the function.
|
||||
suite.Equal(http.StatusForbidden, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
suite.Equal(`{"error":"Forbidden: you do not have permission to boost this status"}`, string(b))
|
||||
// We should have a helpful message.
|
||||
suite.Equal(`{
|
||||
"error": "Forbidden: you do not have permission to boost this status"
|
||||
}`, out)
|
||||
}
|
||||
|
||||
// try to boost a status that's not visible to the user
|
||||
// Try to boost a status that's not visible to the user.
|
||||
func (suite *StatusBoostTestSuite) TestPostNotVisible() {
|
||||
// stop local_account_2 following zork
|
||||
err := suite.db.DeleteByID(context.Background(), suite.testFollows["local_account_2_local_account_1"].ID, >smodel.Follow{})
|
||||
suite.NoError(err)
|
||||
|
||||
t := suite.testTokens["local_account_2"]
|
||||
oauthToken := oauth.DBTokenToToken(t)
|
||||
|
||||
targetStatus := suite.testStatuses["local_account_1_status_3"] // this is a mutual only status and these accounts aren't mutuals
|
||||
|
||||
// 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_2"])
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_2"])
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
|
||||
// normally the router would populate these params from the path values,
|
||||
// but because we're calling the function directly, we need to set them manually.
|
||||
ctx.Params = gin.Params{
|
||||
gin.Param{
|
||||
Key: statuses.IDKey,
|
||||
Value: targetStatus.ID,
|
||||
},
|
||||
// Stop local_account_2 following zork.
|
||||
err := suite.db.DeleteFollowByID(
|
||||
context.Background(),
|
||||
suite.testFollows["local_account_2_local_account_1"].ID,
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.statusModule.StatusBoostPOSTHandler(ctx)
|
||||
var (
|
||||
// This is a mutual only status and
|
||||
// these accounts aren't mutuals anymore.
|
||||
targetStatus = suite.testStatuses["local_account_1_status_3"]
|
||||
app = suite.testApplications["application_1"]
|
||||
token = suite.testTokens["local_account_2"]
|
||||
user = suite.testUsers["local_account_2"]
|
||||
account = suite.testAccounts["local_account_2"]
|
||||
)
|
||||
|
||||
// check response
|
||||
suite.Equal(http.StatusNotFound, recorder.Code) // we 404 statuses that aren't visible
|
||||
out, recorder := suite.postStatusBoost(
|
||||
targetStatus.ID,
|
||||
app,
|
||||
token,
|
||||
user,
|
||||
account,
|
||||
)
|
||||
|
||||
// We should have 404 from
|
||||
// our call to the function.
|
||||
suite.Equal(http.StatusNotFound, recorder.Code)
|
||||
|
||||
// We should have a helpful message.
|
||||
suite.Equal(`{
|
||||
"error": "Not Found: target status not found"
|
||||
}`, out)
|
||||
}
|
||||
|
||||
// Boost a status that's pending approval by us.
|
||||
func (suite *StatusBoostTestSuite) TestPostBoostImplicitAccept() {
|
||||
var (
|
||||
targetStatus = suite.testStatuses["admin_account_status_5"]
|
||||
app = suite.testApplications["application_1"]
|
||||
token = suite.testTokens["local_account_2"]
|
||||
user = suite.testUsers["local_account_2"]
|
||||
account = suite.testAccounts["local_account_2"]
|
||||
)
|
||||
|
||||
out, recorder := suite.postStatusBoost(
|
||||
targetStatus.ID,
|
||||
app,
|
||||
token,
|
||||
user,
|
||||
account,
|
||||
)
|
||||
|
||||
// We should have OK from
|
||||
// our call to the function.
|
||||
suite.Equal(http.StatusOK, recorder.Code)
|
||||
|
||||
// Target status should now
|
||||
// be "reblogged" by us.
|
||||
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": "",
|
||||
"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": null,
|
||||
"media_attachments": [],
|
||||
"mentions": [],
|
||||
"muted": false,
|
||||
"pinned": false,
|
||||
"poll": null,
|
||||
"reblog": {
|
||||
"account": "yeah this is my account, what about it punk",
|
||||
"application": {
|
||||
"name": "superseriousbusiness",
|
||||
"website": "https://superserious.business"
|
||||
},
|
||||
"bookmarked": false,
|
||||
"card": null,
|
||||
"content": "<p>Hi <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>, can I reply?</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": "01F8MHC8VWDRBQR0N1BATDDEM5",
|
||||
"interaction_policy": {
|
||||
"can_favourite": {
|
||||
"always": [
|
||||
"public",
|
||||
"me"
|
||||
],
|
||||
"with_approval": []
|
||||
},
|
||||
"can_reblog": {
|
||||
"always": [
|
||||
"public",
|
||||
"me"
|
||||
],
|
||||
"with_approval": []
|
||||
},
|
||||
"can_reply": {
|
||||
"always": [
|
||||
"public",
|
||||
"me"
|
||||
],
|
||||
"with_approval": []
|
||||
}
|
||||
},
|
||||
"language": null,
|
||||
"media_attachments": [],
|
||||
"mentions": [
|
||||
{
|
||||
"acct": "1happyturtle",
|
||||
"id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
|
||||
"url": "http://localhost:8080/@1happyturtle",
|
||||
"username": "1happyturtle"
|
||||
}
|
||||
],
|
||||
"muted": false,
|
||||
"pinned": false,
|
||||
"poll": null,
|
||||
"reblog": null,
|
||||
"reblogged": true,
|
||||
"reblogs_count": 1,
|
||||
"replies_count": 0,
|
||||
"sensitive": false,
|
||||
"spoiler_text": "",
|
||||
"tags": [],
|
||||
"text": "Hi @1happyturtle, can I reply?",
|
||||
"uri": "http://localhost:8080/some/determinate/url",
|
||||
"url": "http://localhost:8080/some/determinate/url",
|
||||
"visibility": "unlisted"
|
||||
},
|
||||
"reblogged": true,
|
||||
"reblogs_count": 0,
|
||||
"replies_count": 0,
|
||||
"sensitive": false,
|
||||
"spoiler_text": "",
|
||||
"tags": [],
|
||||
"uri": "http://localhost:8080/some/determinate/url",
|
||||
"url": "http://localhost:8080/some/determinate/url",
|
||||
"visibility": "unlisted"
|
||||
}`, out)
|
||||
|
||||
// Target status should no
|
||||
// longer be pending approval.
|
||||
dbStatus, err := suite.state.DB.GetStatusByID(
|
||||
context.Background(),
|
||||
targetStatus.ID,
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
suite.False(*dbStatus.PendingApproval)
|
||||
|
||||
// There should be an Accept
|
||||
// stored for the target status.
|
||||
intReq, err := suite.state.DB.GetInteractionRequestByInteractionURI(
|
||||
context.Background(), targetStatus.URI,
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
suite.NotZero(intReq.AcceptedAt)
|
||||
suite.NotEmpty(intReq.URI)
|
||||
}
|
||||
|
||||
func TestStatusBoostTestSuite(t *testing.T) {
|
||||
|
|
|
@ -20,18 +20,14 @@ package statuses_test
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"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/id"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
@ -81,91 +77,7 @@ func (suite *StatusCreateTestSuite) postStatus(
|
|||
|
||||
// Trigger handler.
|
||||
suite.statusModule.StatusCreatePOSTHandler(ctx)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
rawMap := make(map[string]any)
|
||||
if err := json.Unmarshal(data, &rawMap); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Replace any fields from the raw map that
|
||||
// aren't determinate (date, id, url, etc).
|
||||
if _, ok := rawMap["id"]; ok {
|
||||
rawMap["id"] = id.Highest
|
||||
}
|
||||
|
||||
if _, ok := rawMap["uri"]; ok {
|
||||
rawMap["uri"] = "http://localhost:8080/some/determinate/url"
|
||||
}
|
||||
|
||||
if _, ok := rawMap["url"]; ok {
|
||||
rawMap["url"] = "http://localhost:8080/some/determinate/url"
|
||||
}
|
||||
|
||||
if _, ok := rawMap["created_at"]; ok {
|
||||
rawMap["created_at"] = "right the hell just now babyee"
|
||||
}
|
||||
|
||||
// Make ID of any mentions determinate.
|
||||
if menchiesRaw, ok := rawMap["mentions"]; ok {
|
||||
menchies, ok := menchiesRaw.([]any)
|
||||
if !ok {
|
||||
suite.FailNow("couldn't coerce menchies")
|
||||
}
|
||||
|
||||
for _, menchieRaw := range menchies {
|
||||
menchie, ok := menchieRaw.(map[string]any)
|
||||
if !ok {
|
||||
suite.FailNow("couldn't coerce menchie")
|
||||
}
|
||||
|
||||
if _, ok := menchie["id"]; ok {
|
||||
menchie["id"] = id.Highest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
if _, ok := poll["id"]; ok {
|
||||
poll["id"] = id.Highest
|
||||
}
|
||||
|
||||
if _, ok := poll["expires_at"]; ok {
|
||||
poll["expires_at"] = "ah like you know whatever dude it's chill"
|
||||
}
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
||||
|
||||
// For readability, don't
|
||||
// escape HTML, and indent json.
|
||||
out := new(bytes.Buffer)
|
||||
enc := json.NewEncoder(out)
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.SetIndent("", " ")
|
||||
|
||||
if err := enc.Encode(&rawMap); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
return strings.TrimSpace(out.String()), recorder
|
||||
return suite.parseStatusResponse(recorder)
|
||||
}
|
||||
|
||||
// Post a new status with some custom visibility settings
|
||||
|
@ -447,7 +359,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusMessedUpIntPolicy() {
|
|||
suite.Equal(http.StatusBadRequest, recorder.Code)
|
||||
|
||||
// We should have a helpful error
|
||||
// message telling us how we screwed up.
|
||||
// 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)
|
||||
|
|
|
@ -18,20 +18,18 @@
|
|||
package statuses_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/statuses"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
@ -40,90 +38,260 @@ type StatusFaveTestSuite struct {
|
|||
StatusStandardTestSuite
|
||||
}
|
||||
|
||||
// fave a status
|
||||
func (suite *StatusFaveTestSuite) TestPostFave() {
|
||||
t := suite.testTokens["local_account_1"]
|
||||
oauthToken := oauth.DBTokenToToken(t)
|
||||
|
||||
targetStatus := suite.testStatuses["admin_account_status_2"]
|
||||
|
||||
// setup
|
||||
func (suite *StatusFaveTestSuite) postStatusFave(
|
||||
targetStatusID string,
|
||||
app *gtsmodel.Application,
|
||||
token *gtsmodel.Token,
|
||||
user *gtsmodel.User,
|
||||
account *gtsmodel.Account,
|
||||
) (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.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", strings.Replace(statuses.FavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting
|
||||
ctx.Set(oauth.SessionAuthorizedApplication, app)
|
||||
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(token))
|
||||
ctx.Set(oauth.SessionAuthorizedUser, user)
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, account)
|
||||
|
||||
const pathBase = "http://localhost:8080/api" + statuses.FavouritePath
|
||||
path := strings.ReplaceAll(pathBase, ":"+apiutil.IDKey, targetStatusID)
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, path, nil)
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
|
||||
// normally the router would populate these params from the path values,
|
||||
// but because we're calling the function directly, we need to set them manually.
|
||||
// Populate target status ID.
|
||||
ctx.Params = gin.Params{
|
||||
gin.Param{
|
||||
Key: statuses.IDKey,
|
||||
Value: targetStatus.ID,
|
||||
Key: apiutil.IDKey,
|
||||
Value: targetStatusID,
|
||||
},
|
||||
}
|
||||
|
||||
// Trigger handler.
|
||||
suite.statusModule.StatusFavePOSTHandler(ctx)
|
||||
|
||||
// check response
|
||||
suite.EqualValues(http.StatusOK, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
assert.NoError(suite.T(), err)
|
||||
|
||||
statusReply := &apimodel.Status{}
|
||||
err = json.Unmarshal(b, statusReply)
|
||||
assert.NoError(suite.T(), err)
|
||||
|
||||
assert.Equal(suite.T(), targetStatus.ContentWarning, statusReply.SpoilerText)
|
||||
assert.Equal(suite.T(), targetStatus.Content, statusReply.Content)
|
||||
assert.True(suite.T(), statusReply.Sensitive)
|
||||
assert.Equal(suite.T(), apimodel.VisibilityPublic, statusReply.Visibility)
|
||||
assert.True(suite.T(), statusReply.Favourited)
|
||||
assert.Equal(suite.T(), 1, statusReply.FavouritesCount)
|
||||
return suite.parseStatusResponse(recorder)
|
||||
}
|
||||
|
||||
// try to fave a status that's not faveable
|
||||
// Fave a status we haven't faved yet.
|
||||
func (suite *StatusFaveTestSuite) TestPostFave() {
|
||||
var (
|
||||
targetStatus = suite.testStatuses["admin_account_status_2"]
|
||||
app = suite.testApplications["application_1"]
|
||||
token = suite.testTokens["local_account_1"]
|
||||
user = suite.testUsers["local_account_1"]
|
||||
account = suite.testAccounts["local_account_1"]
|
||||
)
|
||||
|
||||
out, recorder := suite.postStatusFave(
|
||||
targetStatus.ID,
|
||||
app,
|
||||
token,
|
||||
user,
|
||||
account,
|
||||
)
|
||||
|
||||
// We should have OK from
|
||||
// our call to the function.
|
||||
suite.Equal(http.StatusOK, recorder.Code)
|
||||
|
||||
// Target status should now
|
||||
// be "favourited" by us.
|
||||
suite.Equal(`{
|
||||
"account": "yeah this is my account, what about it punk",
|
||||
"application": {
|
||||
"name": "superseriousbusiness",
|
||||
"website": "https://superserious.business"
|
||||
},
|
||||
"bookmarked": false,
|
||||
"card": null,
|
||||
"content": "🐕🐕🐕🐕🐕",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"emojis": [],
|
||||
"favourited": true,
|
||||
"favourites_count": 1,
|
||||
"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": true,
|
||||
"spoiler_text": "open to see some puppies",
|
||||
"tags": [],
|
||||
"text": "🐕🐕🐕🐕🐕",
|
||||
"uri": "http://localhost:8080/some/determinate/url",
|
||||
"url": "http://localhost:8080/some/determinate/url",
|
||||
"visibility": "public"
|
||||
}`, out)
|
||||
}
|
||||
|
||||
// Try to fave a status
|
||||
// that's not faveable by us.
|
||||
func (suite *StatusFaveTestSuite) TestPostUnfaveable() {
|
||||
t := suite.testTokens["admin_account"]
|
||||
oauthToken := oauth.DBTokenToToken(t)
|
||||
var (
|
||||
targetStatus = suite.testStatuses["local_account_1_status_3"]
|
||||
app = suite.testApplications["application_1"]
|
||||
token = suite.testTokens["admin_account"]
|
||||
user = suite.testUsers["admin_account"]
|
||||
account = suite.testAccounts["admin_account"]
|
||||
)
|
||||
|
||||
targetStatus := suite.testStatuses["local_account_1_status_3"] // this one is unlikeable
|
||||
out, recorder := suite.postStatusFave(
|
||||
targetStatus.ID,
|
||||
app,
|
||||
token,
|
||||
user,
|
||||
account,
|
||||
)
|
||||
|
||||
// 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["admin_account"])
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["admin_account"])
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(statuses.FavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
// We should have 403 from
|
||||
// our call to the function.
|
||||
suite.Equal(http.StatusForbidden, recorder.Code)
|
||||
|
||||
// normally the router would populate these params from the path values,
|
||||
// but because we're calling the function directly, we need to set them manually.
|
||||
ctx.Params = gin.Params{
|
||||
gin.Param{
|
||||
Key: statuses.IDKey,
|
||||
Value: targetStatus.ID,
|
||||
},
|
||||
// We should get a helpful error.
|
||||
suite.Equal(`{
|
||||
"error": "Forbidden: you do not have permission to fave this status"
|
||||
}`, out)
|
||||
}
|
||||
|
||||
// Fave a status that's pending approval by us.
|
||||
func (suite *StatusFaveTestSuite) TestPostFaveImplicitAccept() {
|
||||
var (
|
||||
targetStatus = suite.testStatuses["admin_account_status_5"]
|
||||
app = suite.testApplications["application_1"]
|
||||
token = suite.testTokens["local_account_2"]
|
||||
user = suite.testUsers["local_account_2"]
|
||||
account = suite.testAccounts["local_account_2"]
|
||||
)
|
||||
|
||||
out, recorder := suite.postStatusFave(
|
||||
targetStatus.ID,
|
||||
app,
|
||||
token,
|
||||
user,
|
||||
account,
|
||||
)
|
||||
|
||||
// We should have OK from
|
||||
// our call to the function.
|
||||
suite.Equal(http.StatusOK, recorder.Code)
|
||||
|
||||
// Target status should now
|
||||
// be "favourited" by us.
|
||||
suite.Equal(`{
|
||||
"account": "yeah this is my account, what about it punk",
|
||||
"application": {
|
||||
"name": "superseriousbusiness",
|
||||
"website": "https://superserious.business"
|
||||
},
|
||||
"bookmarked": false,
|
||||
"card": null,
|
||||
"content": "<p>Hi <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>, can I reply?</p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"emojis": [],
|
||||
"favourited": true,
|
||||
"favourites_count": 1,
|
||||
"id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
|
||||
"in_reply_to_account_id": "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||
"in_reply_to_id": "01F8MHC8VWDRBQR0N1BATDDEM5",
|
||||
"interaction_policy": {
|
||||
"can_favourite": {
|
||||
"always": [
|
||||
"public",
|
||||
"me"
|
||||
],
|
||||
"with_approval": []
|
||||
},
|
||||
"can_reblog": {
|
||||
"always": [
|
||||
"public",
|
||||
"me"
|
||||
],
|
||||
"with_approval": []
|
||||
},
|
||||
"can_reply": {
|
||||
"always": [
|
||||
"public",
|
||||
"me"
|
||||
],
|
||||
"with_approval": []
|
||||
}
|
||||
},
|
||||
"language": null,
|
||||
"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": "Hi @1happyturtle, can I reply?",
|
||||
"uri": "http://localhost:8080/some/determinate/url",
|
||||
"url": "http://localhost:8080/some/determinate/url",
|
||||
"visibility": "unlisted"
|
||||
}`, out)
|
||||
|
||||
// Target status should no
|
||||
// longer be pending approval.
|
||||
dbStatus, err := suite.state.DB.GetStatusByID(
|
||||
context.Background(),
|
||||
targetStatus.ID,
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
suite.False(*dbStatus.PendingApproval)
|
||||
|
||||
suite.statusModule.StatusFavePOSTHandler(ctx)
|
||||
|
||||
// check response
|
||||
suite.EqualValues(http.StatusForbidden, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.Equal(suite.T(), `{"error":"Forbidden: you do not have permission to fave this status"}`, string(b))
|
||||
// There should be an Accept
|
||||
// stored for the target status.
|
||||
intReq, err := suite.state.DB.GetInteractionRequestByInteractionURI(
|
||||
context.Background(), targetStatus.URI,
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
suite.NotZero(intReq.AcceptedAt)
|
||||
suite.NotEmpty(intReq.URI)
|
||||
}
|
||||
|
||||
func TestStatusFaveTestSuite(t *testing.T) {
|
||||
|
|
|
@ -223,7 +223,7 @@ func NewProcessor(
|
|||
processor.tags = tags.New(state, converter)
|
||||
processor.timeline = timeline.New(state, converter, visFilter)
|
||||
processor.search = search.New(state, federator, converter, visFilter)
|
||||
processor.status = status.New(state, &common, &processor.polls, federator, converter, visFilter, intFilter, parseMentionFunc)
|
||||
processor.status = status.New(state, &common, &processor.polls, &processor.interactionRequests, federator, converter, visFilter, intFilter, parseMentionFunc)
|
||||
processor.user = user.New(state, converter, oauthServer, emailSender)
|
||||
|
||||
// The advanced migrations processor sequences advanced migrations from all other processors.
|
||||
|
|
|
@ -28,6 +28,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// BoostCreate processes the boost/reblog of target
|
||||
|
@ -138,6 +139,23 @@ func (p *Processor) BoostCreate(
|
|||
Target: target.Account,
|
||||
})
|
||||
|
||||
// If the boost target status replies to a status
|
||||
// that we own, and has a pending interaction
|
||||
// request, use the boost as an implicit accept.
|
||||
implicitlyAccepted, errWithCode := p.implicitlyAccept(ctx,
|
||||
requester, target,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
return nil, errWithCode
|
||||
}
|
||||
|
||||
// If we ended up implicitly accepting, mark the
|
||||
// target status as no longer pending approval so
|
||||
// it's serialized properly via the API.
|
||||
if implicitlyAccepted {
|
||||
target.PendingApproval = util.Ptr(false)
|
||||
}
|
||||
|
||||
return p.c.GetAPIStatus(ctx, requester, boost)
|
||||
}
|
||||
|
||||
|
|
|
@ -164,6 +164,23 @@ func (p *Processor) Create(
|
|||
}
|
||||
}
|
||||
|
||||
// If the new status replies to a status that
|
||||
// replies to us, use our reply as an implicit
|
||||
// accept of any pending interaction.
|
||||
implicitlyAccepted, errWithCode := p.implicitlyAccept(ctx,
|
||||
requester, status,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
return nil, errWithCode
|
||||
}
|
||||
|
||||
// If we ended up implicitly accepting, mark the
|
||||
// replied-to status as no longer pending approval
|
||||
// so it's serialized properly via the API.
|
||||
if implicitlyAccepted {
|
||||
status.InReplyTo.PendingApproval = util.Ptr(false)
|
||||
}
|
||||
|
||||
return p.c.GetAPIStatus(ctx, requester, status)
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
func (p *Processor) getFaveableStatus(
|
||||
|
@ -138,8 +139,6 @@ func (p *Processor) FaveCreate(
|
|||
pendingApproval = false
|
||||
}
|
||||
|
||||
status.PendingApproval = &pendingApproval
|
||||
|
||||
// Create a new fave, marking it
|
||||
// as pending approval if necessary.
|
||||
faveID := id.NewULID()
|
||||
|
@ -157,7 +156,7 @@ func (p *Processor) FaveCreate(
|
|||
}
|
||||
|
||||
if err := p.state.DB.PutStatusFave(ctx, gtsFave); err != nil {
|
||||
err = fmt.Errorf("FaveCreate: error putting fave in database: %w", err)
|
||||
err = gtserror.Newf("db error putting fave: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
|
@ -170,6 +169,23 @@ func (p *Processor) FaveCreate(
|
|||
Target: status.Account,
|
||||
})
|
||||
|
||||
// If the fave target status replies to a status
|
||||
// that we own, and has a pending interaction
|
||||
// request, use the fave as an implicit accept.
|
||||
implicitlyAccepted, errWithCode := p.implicitlyAccept(ctx,
|
||||
requester, status,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
return nil, errWithCode
|
||||
}
|
||||
|
||||
// If we ended up implicitly accepting, mark the
|
||||
// target status as no longer pending approval so
|
||||
// it's serialized properly via the API.
|
||||
if implicitlyAccepted {
|
||||
status.PendingApproval = util.Ptr(false)
|
||||
}
|
||||
|
||||
return p.c.GetAPIStatus(ctx, requester, status)
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/interactionrequests"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/polls"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/text"
|
||||
|
@ -42,7 +43,8 @@ type Processor struct {
|
|||
parseMention gtsmodel.ParseMentionFunc
|
||||
|
||||
// other processors
|
||||
polls *polls.Processor
|
||||
polls *polls.Processor
|
||||
intReqs *interactionrequests.Processor
|
||||
}
|
||||
|
||||
// New returns a new status processor.
|
||||
|
@ -50,6 +52,7 @@ func New(
|
|||
state *state.State,
|
||||
common *common.Processor,
|
||||
polls *polls.Processor,
|
||||
intReqs *interactionrequests.Processor,
|
||||
federator *federation.Federator,
|
||||
converter *typeutils.Converter,
|
||||
visFilter *visibility.Filter,
|
||||
|
@ -66,5 +69,6 @@ func New(
|
|||
formatter: text.NewFormatter(state.DB),
|
||||
parseMention: parseMention,
|
||||
polls: polls,
|
||||
intReqs: intReqs,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/interactionrequests"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/polls"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/status"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
|
@ -100,11 +101,13 @@ func (suite *StatusStandardTestSuite) SetupTest() {
|
|||
|
||||
common := common.New(&suite.state, suite.mediaManager, suite.typeConverter, suite.federator, visFilter)
|
||||
polls := polls.New(&common, &suite.state, suite.typeConverter)
|
||||
intReqs := interactionrequests.New(&common, &suite.state, suite.typeConverter)
|
||||
|
||||
suite.status = status.New(
|
||||
&suite.state,
|
||||
&common,
|
||||
&polls,
|
||||
&intReqs,
|
||||
suite.federator,
|
||||
suite.typeConverter,
|
||||
visFilter,
|
||||
|
|
72
internal/processing/status/util.go
Normal file
72
internal/processing/status/util.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package status
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
func (p *Processor) implicitlyAccept(
|
||||
ctx context.Context,
|
||||
requester *gtsmodel.Account,
|
||||
status *gtsmodel.Status,
|
||||
) (bool, gtserror.WithCode) {
|
||||
if status.InReplyToAccountID != requester.ID {
|
||||
// Status doesn't reply to us,
|
||||
// we can't accept on behalf
|
||||
// of someone else.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
targetPendingApproval := util.PtrOrValue(status.PendingApproval, false)
|
||||
if !targetPendingApproval {
|
||||
// Status isn't pending approval,
|
||||
// nothing to implicitly accept.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Status is pending approval,
|
||||
// check for an interaction request.
|
||||
intReq, err := p.state.DB.GetInteractionRequestByInteractionURI(ctx, status.URI)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
// Something's gone wrong.
|
||||
err := gtserror.Newf("db error getting interaction request for %s: %w", status.URI, err)
|
||||
return false, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// No interaction request present
|
||||
// for this status. Race condition?
|
||||
if intReq == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Accept the interaction.
|
||||
if _, errWithCode := p.intReqs.Accept(ctx,
|
||||
requester, intReq.ID,
|
||||
); errWithCode != nil {
|
||||
return false, errWithCode
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
|
@ -800,26 +800,55 @@ func (c *Converter) TagToAPITag(ctx context.Context, t *gtsmodel.Tag, stubHistor
|
|||
}, nil
|
||||
}
|
||||
|
||||
// StatusToAPIStatus converts a gts model status into its api
|
||||
// (frontend) representation for serialization on the API.
|
||||
// StatusToAPIStatus converts a gts model
|
||||
// status into its api (frontend) representation
|
||||
// for serialization on the API.
|
||||
//
|
||||
// Requesting account can be nil.
|
||||
//
|
||||
// Filter context can be the empty string if these statuses are not being filtered.
|
||||
// filterContext can be the empty string
|
||||
// if these statuses are not being filtered.
|
||||
//
|
||||
// If there is a matching "hide" filter, the returned status will be nil with a ErrHideStatus error;
|
||||
// callers need to handle that case by excluding it from results.
|
||||
// If there is a matching "hide" filter, the returned
|
||||
// status will be nil with a ErrHideStatus error; callers
|
||||
// need to handle that case by excluding it from results.
|
||||
func (c *Converter) StatusToAPIStatus(
|
||||
ctx context.Context,
|
||||
s *gtsmodel.Status,
|
||||
status *gtsmodel.Status,
|
||||
requestingAccount *gtsmodel.Account,
|
||||
filterContext statusfilter.FilterContext,
|
||||
filters []*gtsmodel.Filter,
|
||||
mutes *usermute.CompiledUserMuteList,
|
||||
) (*apimodel.Status, error) {
|
||||
return c.statusToAPIStatus(
|
||||
ctx,
|
||||
status,
|
||||
requestingAccount,
|
||||
filterContext,
|
||||
filters,
|
||||
mutes,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
// statusToAPIStatus is the package-internal implementation
|
||||
// of StatusToAPIStatus that lets the caller customize whether
|
||||
// to placehold unknown attachment types, and/or add a note
|
||||
// about the status being pending and requiring approval.
|
||||
func (c *Converter) statusToAPIStatus(
|
||||
ctx context.Context,
|
||||
status *gtsmodel.Status,
|
||||
requestingAccount *gtsmodel.Account,
|
||||
filterContext statusfilter.FilterContext,
|
||||
filters []*gtsmodel.Filter,
|
||||
mutes *usermute.CompiledUserMuteList,
|
||||
placeholdAttachments bool,
|
||||
addPendingNote bool,
|
||||
) (*apimodel.Status, error) {
|
||||
apiStatus, err := c.statusToFrontend(
|
||||
ctx,
|
||||
s,
|
||||
status,
|
||||
requestingAccount, // Can be nil.
|
||||
filterContext, // Can be empty.
|
||||
filters,
|
||||
|
@ -830,7 +859,7 @@ func (c *Converter) StatusToAPIStatus(
|
|||
}
|
||||
|
||||
// Convert author to API model.
|
||||
acct, err := c.AccountToAPIAccountPublic(ctx, s.Account)
|
||||
acct, err := c.AccountToAPIAccountPublic(ctx, status.Account)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error converting status acct: %w", err)
|
||||
}
|
||||
|
@ -839,23 +868,43 @@ func (c *Converter) StatusToAPIStatus(
|
|||
// Convert author of boosted
|
||||
// status (if set) to API model.
|
||||
if apiStatus.Reblog != nil {
|
||||
boostAcct, err := c.AccountToAPIAccountPublic(ctx, s.BoostOfAccount)
|
||||
boostAcct, err := c.AccountToAPIAccountPublic(ctx, status.BoostOfAccount)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error converting boost acct: %w", err)
|
||||
}
|
||||
apiStatus.Reblog.Account = boostAcct
|
||||
}
|
||||
|
||||
// Normalize status for API by pruning
|
||||
// attachments that were not locally
|
||||
// stored, replacing them with a helpful
|
||||
// message + links to remote.
|
||||
var aside string
|
||||
aside, apiStatus.MediaAttachments = placeholderAttachments(apiStatus.MediaAttachments)
|
||||
apiStatus.Content += aside
|
||||
if apiStatus.Reblog != nil {
|
||||
aside, apiStatus.Reblog.MediaAttachments = placeholderAttachments(apiStatus.Reblog.MediaAttachments)
|
||||
apiStatus.Reblog.Content += aside
|
||||
if placeholdAttachments {
|
||||
// Normalize status for API by pruning attachments
|
||||
// that were not able to be locally stored, and replacing
|
||||
// them with a helpful message + links to remote.
|
||||
var attachNote string
|
||||
attachNote, apiStatus.MediaAttachments = placeholderAttachments(apiStatus.MediaAttachments)
|
||||
apiStatus.Content += attachNote
|
||||
|
||||
// Do the same for the reblogged status.
|
||||
if apiStatus.Reblog != nil {
|
||||
attachNote, apiStatus.Reblog.MediaAttachments = placeholderAttachments(apiStatus.Reblog.MediaAttachments)
|
||||
apiStatus.Reblog.Content += attachNote
|
||||
}
|
||||
}
|
||||
|
||||
if addPendingNote {
|
||||
// If this status is pending approval and
|
||||
// replies to the requester, add a note
|
||||
// about how to approve or reject the reply.
|
||||
pendingApproval := util.PtrOrValue(status.PendingApproval, false)
|
||||
if pendingApproval &&
|
||||
requestingAccount != nil &&
|
||||
requestingAccount.ID == status.InReplyToAccountID {
|
||||
pendingNote, err := c.pendingReplyNote(ctx, status)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error deriving 'pending reply' note: %w", err)
|
||||
}
|
||||
|
||||
apiStatus.Content += pendingNote
|
||||
}
|
||||
}
|
||||
|
||||
return apiStatus, nil
|
||||
|
@ -1972,7 +2021,20 @@ func (c *Converter) ReportToAdminAPIReport(ctx context.Context, r *gtsmodel.Repo
|
|||
}
|
||||
}
|
||||
for _, s := range r.Statuses {
|
||||
status, err := c.StatusToAPIStatus(ctx, s, requestingAccount, statusfilter.FilterContextNone, nil, nil)
|
||||
status, err := c.statusToAPIStatus(
|
||||
ctx,
|
||||
s,
|
||||
requestingAccount,
|
||||
statusfilter.FilterContextNone,
|
||||
nil, // No filters.
|
||||
nil, // No mutes.
|
||||
true, // Placehold unknown attachments.
|
||||
|
||||
// Don't add note about
|
||||
// pending, it's not
|
||||
// relevant here.
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ReportToAdminAPIReport: error converting status with id %s to api status: %w", s.ID, err)
|
||||
}
|
||||
|
@ -2609,8 +2671,8 @@ func (c *Converter) InteractionReqToAPIInteractionReq(
|
|||
req.Status,
|
||||
requestingAcct,
|
||||
statusfilter.FilterContextNone,
|
||||
nil,
|
||||
nil,
|
||||
nil, // No filters.
|
||||
nil, // No mutes.
|
||||
)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error converting interacted status: %w", err)
|
||||
|
@ -2619,13 +2681,20 @@ func (c *Converter) InteractionReqToAPIInteractionReq(
|
|||
|
||||
var reply *apimodel.Status
|
||||
if req.InteractionType == gtsmodel.InteractionReply {
|
||||
reply, err = c.StatusToAPIStatus(
|
||||
reply, err = c.statusToAPIStatus(
|
||||
ctx,
|
||||
req.Reply,
|
||||
req.Status,
|
||||
requestingAcct,
|
||||
statusfilter.FilterContextNone,
|
||||
nil,
|
||||
nil,
|
||||
nil, // No filters.
|
||||
nil, // No mutes.
|
||||
true, // Placehold unknown attachments.
|
||||
|
||||
// Don't add note about pending;
|
||||
// requester already knows it's
|
||||
// pending because they're looking
|
||||
// at the request right now.
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error converting reply: %w", err)
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
package typeutils_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
@ -1708,6 +1709,130 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendPartialInteraction
|
|||
}`, string(b))
|
||||
}
|
||||
|
||||
func (suite *InternalToFrontendTestSuite) TestStatusToAPIStatusPendingApproval() {
|
||||
var (
|
||||
testStatus = suite.testStatuses["admin_account_status_5"]
|
||||
requestingAccount = suite.testAccounts["local_account_2"]
|
||||
)
|
||||
|
||||
apiStatus, err := suite.typeconverter.StatusToAPIStatus(
|
||||
context.Background(),
|
||||
testStatus,
|
||||
requestingAccount,
|
||||
statusfilter.FilterContextNone,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// We want to see the HTML in
|
||||
// the status so don't escape it.
|
||||
out := new(bytes.Buffer)
|
||||
enc := json.NewEncoder(out)
|
||||
enc.SetIndent("", " ")
|
||||
enc.SetEscapeHTML(false)
|
||||
if err := enc.Encode(apiStatus); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Equal(`{
|
||||
"id": "01J5QVB9VC76NPPRQ207GG4DRZ",
|
||||
"created_at": "2024-02-20T10:41:37.000Z",
|
||||
"in_reply_to_id": "01F8MHC8VWDRBQR0N1BATDDEM5",
|
||||
"in_reply_to_account_id": "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||
"sensitive": false,
|
||||
"spoiler_text": "",
|
||||
"visibility": "unlisted",
|
||||
"language": null,
|
||||
"uri": "http://localhost:8080/users/admin/statuses/01J5QVB9VC76NPPRQ207GG4DRZ",
|
||||
"url": "http://localhost:8080/@admin/statuses/01J5QVB9VC76NPPRQ207GG4DRZ",
|
||||
"replies_count": 0,
|
||||
"reblogs_count": 0,
|
||||
"favourites_count": 0,
|
||||
"favourited": false,
|
||||
"reblogged": false,
|
||||
"muted": false,
|
||||
"bookmarked": false,
|
||||
"pinned": false,
|
||||
"content": "<p>Hi <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>, can I reply?</p><hr><p><i lang=\"en\">ℹ️ Note from localhost:8080: This reply to your status is pending your approval. You can accept the reply by liking, replying to, or boosting it. You can also accept or reject the reply at the following link: <a href=\"http://localhost:8080/settings/user/interaction_requests/01J5QVXCCEATJYSXM9H6MZT4JR\" rel=\"noreferrer noopener nofollow\" target=\"_blank\">http://localhost:8080/settings/user/interaction_requests/01J5QVXCCEATJYSXM9H6MZT4JR (opens in a new tab)</a>.</i></p>",
|
||||
"reblog": null,
|
||||
"application": {
|
||||
"name": "superseriousbusiness",
|
||||
"website": "https://superserious.business"
|
||||
},
|
||||
"account": {
|
||||
"id": "01F8MH17FWEB39HZJ76B6VXSKF",
|
||||
"username": "admin",
|
||||
"acct": "admin",
|
||||
"display_name": "",
|
||||
"locked": false,
|
||||
"discoverable": true,
|
||||
"bot": false,
|
||||
"created_at": "2022-05-17T13:10:59.000Z",
|
||||
"note": "",
|
||||
"url": "http://localhost:8080/@admin",
|
||||
"avatar": "",
|
||||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2021-10-20T10:41:37.000Z",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true,
|
||||
"roles": [
|
||||
{
|
||||
"id": "admin",
|
||||
"name": "admin",
|
||||
"color": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"media_attachments": [],
|
||||
"mentions": [
|
||||
{
|
||||
"id": "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||
"username": "1happyturtle",
|
||||
"url": "http://localhost:8080/@1happyturtle",
|
||||
"acct": "1happyturtle"
|
||||
}
|
||||
],
|
||||
"tags": [],
|
||||
"emojis": [],
|
||||
"card": null,
|
||||
"poll": null,
|
||||
"text": "Hi @1happyturtle, can I reply?",
|
||||
"interaction_policy": {
|
||||
"can_favourite": {
|
||||
"always": [
|
||||
"public",
|
||||
"me"
|
||||
],
|
||||
"with_approval": []
|
||||
},
|
||||
"can_reply": {
|
||||
"always": [
|
||||
"public",
|
||||
"me"
|
||||
],
|
||||
"with_approval": []
|
||||
},
|
||||
"can_reblog": {
|
||||
"always": [
|
||||
"public",
|
||||
"me"
|
||||
],
|
||||
"with_approval": []
|
||||
}
|
||||
}
|
||||
}
|
||||
`, out.String())
|
||||
}
|
||||
|
||||
func (suite *InternalToFrontendTestSuite) TestVideoAttachmentToFrontend() {
|
||||
testAttachment := suite.testAttachments["local_account_1_status_4_attachment_2"]
|
||||
apiAttachment, err := suite.typeconverter.AttachmentToAPIAttachment(context.Background(), testAttachment)
|
||||
|
|
|
@ -19,6 +19,7 @@ package typeutils
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/url"
|
||||
|
@ -30,6 +31,8 @@ import (
|
|||
"github.com/k3a/html2text"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/language"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
|
@ -187,6 +190,47 @@ func placeholderAttachments(arr []*apimodel.Attachment) (string, []*apimodel.Att
|
|||
return text.SanitizeToHTML(note.String()), arr
|
||||
}
|
||||
|
||||
func (c *Converter) pendingReplyNote(
|
||||
ctx context.Context,
|
||||
s *gtsmodel.Status,
|
||||
) (string, error) {
|
||||
intReq, err := c.state.DB.GetInteractionRequestByInteractionURI(ctx, s.URI)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
// Something's gone wrong.
|
||||
err := gtserror.Newf("db error getting interaction request for %s: %w", s.URI, err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// No interaction request present
|
||||
// for this status. Race condition?
|
||||
if intReq == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var (
|
||||
proto = config.GetProtocol()
|
||||
host = config.GetHost()
|
||||
|
||||
// Build the settings panel URL at which the user
|
||||
// can view + approve/reject the interaction request.
|
||||
//
|
||||
// Eg., https://example.org/settings/user/interaction_requests/01J5QVXCCEATJYSXM9H6MZT4JR
|
||||
settingsURL = proto + "://" + host + "/settings/user/interaction_requests/" + intReq.ID
|
||||
)
|
||||
|
||||
var note strings.Builder
|
||||
note.WriteString(`<hr>`)
|
||||
note.WriteString(`<p><i lang="en">ℹ️ Note from ` + host + `: `)
|
||||
note.WriteString(`This reply to your status is pending your approval. You can accept the reply by liking, replying to, or boosting it. You can also accept or reject the reply at the following link: `)
|
||||
note.WriteString(`<a href="` + settingsURL + `" `)
|
||||
note.WriteString(`rel="noreferrer noopener" target="_blank">`)
|
||||
note.WriteString(settingsURL + ` (opens in a new tab)`)
|
||||
note.WriteString(`</a>.`)
|
||||
note.WriteString(`</i></p>`)
|
||||
|
||||
return text.SanitizeToHTML(note.String()), nil
|
||||
}
|
||||
|
||||
// ContentToContentLanguage tries to
|
||||
// extract a content string and language
|
||||
// tag string from the given intermediary
|
||||
|
|
|
@ -52,10 +52,10 @@ export default function UserRouter() {
|
|||
<Route path="/emailpassword" component={EmailPassword} />
|
||||
<Route path="/migration" component={UserMigration} />
|
||||
<Route path="/export-import" component={ExportImport} />
|
||||
<InteractionRequestsRouter />
|
||||
<Route><Redirect to="/profile" /></Route>
|
||||
</Switch>
|
||||
</ErrorBoundary>
|
||||
<InteractionRequestsRouter />
|
||||
</Router>
|
||||
</BaseUrlContext.Provider>
|
||||
);
|
||||
|
@ -73,13 +73,11 @@ function InteractionRequestsRouter() {
|
|||
return (
|
||||
<BaseUrlContext.Provider value={absBase}>
|
||||
<Router base={thisBase}>
|
||||
<ErrorBoundary>
|
||||
<Switch>
|
||||
<Route path="/search" component={InteractionRequests} />
|
||||
<Route path="/:reqId" component={InteractionRequestDetail} />
|
||||
<Route><Redirect to="/search"/></Route>
|
||||
</Switch>
|
||||
</ErrorBoundary>
|
||||
<Switch>
|
||||
<Route path="/search" component={InteractionRequests} />
|
||||
<Route path="/:reqId" component={InteractionRequestDetail} />
|
||||
<Route><Redirect to="/search"/></Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
</BaseUrlContext.Provider>
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue