[feature] Show info for pending replies, allow implicit accept of pending replies (#3322)
* [feature] Allow implicit accept of pending replies * update wording
This commit is contained in:
parent
2f13b72e2e
commit
1ce854358d
|
@ -18,6 +18,12 @@
|
||||||
package statuses_test
|
package statuses_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/statuses"
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/statuses"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
@ -25,6 +31,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
|
@ -59,6 +66,113 @@ type StatusStandardTestSuite struct {
|
||||||
statusModule *statuses.Module
|
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() {
|
func (suite *StatusStandardTestSuite) SetupSuite() {
|
||||||
suite.testTokens = testrig.NewTestTokens()
|
suite.testTokens = testrig.NewTestTokens()
|
||||||
suite.testClients = testrig.NewTestClients()
|
suite.testClients = testrig.NewTestClients()
|
||||||
|
|
|
@ -17,9 +17,6 @@ package statuses_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -28,7 +25,7 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/statuses"
|
"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/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
|
@ -38,212 +35,596 @@ type StatusBoostTestSuite struct {
|
||||||
StatusStandardTestSuite
|
StatusStandardTestSuite
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *StatusBoostTestSuite) TestPostBoost() {
|
func (suite *StatusBoostTestSuite) postStatusBoost(
|
||||||
t := suite.testTokens["local_account_1"]
|
targetStatusID string,
|
||||||
oauthToken := oauth.DBTokenToToken(t)
|
app *gtsmodel.Application,
|
||||||
|
token *gtsmodel.Token,
|
||||||
targetStatus := suite.testStatuses["admin_account_status_1"]
|
user *gtsmodel.User,
|
||||||
|
account *gtsmodel.Account,
|
||||||
// setup
|
) (string, *httptest.ResponseRecorder) {
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
ctx.Set(oauth.SessionAuthorizedApplication, app)
|
||||||
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(token))
|
||||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
ctx.Set(oauth.SessionAuthorizedUser, user)
|
||||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
ctx.Set(oauth.SessionAuthorizedAccount, account)
|
||||||
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
|
|
||||||
|
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")
|
ctx.Request.Header.Set("accept", "application/json")
|
||||||
|
|
||||||
// normally the router would populate these params from the path values,
|
// Populate target status ID.
|
||||||
// but because we're calling the function directly, we need to set them manually.
|
|
||||||
ctx.Params = gin.Params{
|
ctx.Params = gin.Params{
|
||||||
gin.Param{
|
gin.Param{
|
||||||
Key: statuses.IDKey,
|
Key: apiutil.IDKey,
|
||||||
Value: targetStatus.ID,
|
Value: targetStatusID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trigger handler.
|
||||||
suite.statusModule.StatusBoostPOSTHandler(ctx)
|
suite.statusModule.StatusBoostPOSTHandler(ctx)
|
||||||
|
return suite.parseStatusResponse(recorder)
|
||||||
|
}
|
||||||
|
|
||||||
// check response
|
func (suite *StatusBoostTestSuite) TestPostBoost() {
|
||||||
suite.EqualValues(http.StatusOK, recorder.Code)
|
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()
|
out, recorder := suite.postStatusBoost(
|
||||||
defer result.Body.Close()
|
targetStatus.ID,
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
app,
|
||||||
suite.NoError(err)
|
token,
|
||||||
|
user,
|
||||||
|
account,
|
||||||
|
)
|
||||||
|
|
||||||
statusReply := &apimodel.Status{}
|
// We should have OK from
|
||||||
err = json.Unmarshal(b, statusReply)
|
// our call to the function.
|
||||||
suite.NoError(err)
|
suite.Equal(http.StatusOK, recorder.Code)
|
||||||
|
|
||||||
suite.False(statusReply.Sensitive)
|
// Target status should now
|
||||||
suite.Equal(apimodel.VisibilityPublic, statusReply.Visibility)
|
// be "reblogged" by us.
|
||||||
|
suite.Equal(`{
|
||||||
suite.Empty(statusReply.SpoilerText)
|
"account": "yeah this is my account, what about it punk",
|
||||||
suite.Empty(statusReply.Content)
|
"application": {
|
||||||
suite.Equal("the_mighty_zork", statusReply.Account.Username)
|
"name": "really cool gts application",
|
||||||
suite.Len(statusReply.MediaAttachments, 0)
|
"website": "https://reallycool.app"
|
||||||
suite.Len(statusReply.Mentions, 0)
|
},
|
||||||
suite.Len(statusReply.Emojis, 0)
|
"bookmarked": true,
|
||||||
suite.Len(statusReply.Tags, 0)
|
"card": null,
|
||||||
|
"content": "",
|
||||||
suite.NotNil(statusReply.Application)
|
"created_at": "right the hell just now babyee",
|
||||||
suite.Equal("really cool gts application", statusReply.Application.Name)
|
"emojis": [],
|
||||||
|
"favourited": true,
|
||||||
suite.NotNil(statusReply.Reblog)
|
"favourites_count": 0,
|
||||||
suite.Equal(1, statusReply.Reblog.ReblogsCount)
|
"id": "ZZZZZZZZZZZZZZZZZZZZZZZZZZ",
|
||||||
suite.Equal(1, statusReply.Reblog.FavouritesCount)
|
"in_reply_to_account_id": null,
|
||||||
suite.Equal(targetStatus.Content, statusReply.Reblog.Content)
|
"in_reply_to_id": null,
|
||||||
suite.Equal(targetStatus.ContentWarning, statusReply.Reblog.SpoilerText)
|
"interaction_policy": {
|
||||||
suite.Equal(targetStatus.AccountID, statusReply.Reblog.Account.ID)
|
"can_favourite": {
|
||||||
suite.Len(statusReply.Reblog.MediaAttachments, 1)
|
"always": [
|
||||||
suite.Len(statusReply.Reblog.Tags, 1)
|
"public",
|
||||||
suite.Len(statusReply.Reblog.Emojis, 1)
|
"me"
|
||||||
suite.True(statusReply.Reblogged)
|
],
|
||||||
suite.True(statusReply.Reblog.Reblogged)
|
"with_approval": []
|
||||||
suite.Equal("superseriousbusiness", statusReply.Reblog.Application.Name)
|
},
|
||||||
|
"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() {
|
func (suite *StatusBoostTestSuite) TestPostBoostOwnFollowersOnly() {
|
||||||
t := suite.testTokens["local_account_1"]
|
var (
|
||||||
oauthToken := oauth.DBTokenToToken(t)
|
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"]
|
out, recorder := suite.postStatusBoost(
|
||||||
testAccount := suite.testAccounts["local_account_1"]
|
targetStatus.ID,
|
||||||
testUser := suite.testUsers["local_account_1"]
|
app,
|
||||||
|
token,
|
||||||
|
user,
|
||||||
|
account,
|
||||||
|
)
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
// We should have OK from
|
||||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
// our call to the function.
|
||||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
suite.Equal(http.StatusOK, recorder.Code)
|
||||||
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")
|
|
||||||
|
|
||||||
ctx.Params = gin.Params{
|
// Target status should now
|
||||||
gin.Param{
|
// be "reblogged" by us.
|
||||||
Key: statuses.IDKey,
|
suite.Equal(`{
|
||||||
Value: testStatus.ID,
|
"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)
|
||||||
}
|
}
|
||||||
|
|
||||||
suite.statusModule.StatusBoostPOSTHandler(ctx)
|
// Try to boost a status that's
|
||||||
|
// not boostable / visible to us.
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to boost a status that's not boostable / visible to us
|
|
||||||
func (suite *StatusBoostTestSuite) TestPostUnboostable() {
|
func (suite *StatusBoostTestSuite) TestPostUnboostable() {
|
||||||
t := suite.testTokens["local_account_1"]
|
var (
|
||||||
oauthToken := oauth.DBTokenToToken(t)
|
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
|
// We should have 403 from
|
||||||
recorder := httptest.NewRecorder()
|
// our call to the function.
|
||||||
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
|
|
||||||
suite.Equal(http.StatusForbidden, recorder.Code)
|
suite.Equal(http.StatusForbidden, recorder.Code)
|
||||||
|
|
||||||
result := recorder.Result()
|
// We should have a helpful message.
|
||||||
defer result.Body.Close()
|
suite.Equal(`{
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
"error": "Forbidden: you do not have permission to boost this status"
|
||||||
suite.NoError(err)
|
}`, out)
|
||||||
suite.Equal(`{"error":"Forbidden: you do not have permission to boost this status"}`, string(b))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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() {
|
func (suite *StatusBoostTestSuite) TestPostNotVisible() {
|
||||||
// stop local_account_2 following zork
|
// Stop local_account_2 following zork.
|
||||||
err := suite.db.DeleteByID(context.Background(), suite.testFollows["local_account_2_local_account_1"].ID, >smodel.Follow{})
|
err := suite.db.DeleteFollowByID(
|
||||||
suite.NoError(err)
|
context.Background(),
|
||||||
|
suite.testFollows["local_account_2_local_account_1"].ID,
|
||||||
t := suite.testTokens["local_account_2"]
|
)
|
||||||
oauthToken := oauth.DBTokenToToken(t)
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
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,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
out, recorder := suite.postStatusBoost(
|
||||||
suite.Equal(http.StatusNotFound, recorder.Code) // we 404 statuses that aren't visible
|
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) {
|
func TestStatusBoostTestSuite(t *testing.T) {
|
||||||
|
|
|
@ -20,18 +20,14 @@ package statuses_test
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/statuses"
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/statuses"
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
)
|
)
|
||||||
|
@ -81,91 +77,7 @@ func (suite *StatusCreateTestSuite) postStatus(
|
||||||
|
|
||||||
// Trigger handler.
|
// Trigger handler.
|
||||||
suite.statusModule.StatusCreatePOSTHandler(ctx)
|
suite.statusModule.StatusCreatePOSTHandler(ctx)
|
||||||
|
return suite.parseStatusResponse(recorder)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post a new status with some custom visibility settings
|
// Post a new status with some custom visibility settings
|
||||||
|
|
|
@ -18,20 +18,18 @@
|
||||||
package statuses_test
|
package statuses_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"context"
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/statuses"
|
"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/internal/oauth"
|
||||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
)
|
)
|
||||||
|
@ -40,90 +38,260 @@ type StatusFaveTestSuite struct {
|
||||||
StatusStandardTestSuite
|
StatusStandardTestSuite
|
||||||
}
|
}
|
||||||
|
|
||||||
// fave a status
|
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, 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")
|
||||||
|
|
||||||
|
// Populate target status ID.
|
||||||
|
ctx.Params = gin.Params{
|
||||||
|
gin.Param{
|
||||||
|
Key: apiutil.IDKey,
|
||||||
|
Value: targetStatusID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger handler.
|
||||||
|
suite.statusModule.StatusFavePOSTHandler(ctx)
|
||||||
|
return suite.parseStatusResponse(recorder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fave a status we haven't faved yet.
|
||||||
func (suite *StatusFaveTestSuite) TestPostFave() {
|
func (suite *StatusFaveTestSuite) TestPostFave() {
|
||||||
t := suite.testTokens["local_account_1"]
|
var (
|
||||||
oauthToken := oauth.DBTokenToToken(t)
|
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"]
|
||||||
|
)
|
||||||
|
|
||||||
targetStatus := suite.testStatuses["admin_account_status_2"]
|
out, recorder := suite.postStatusFave(
|
||||||
|
targetStatus.ID,
|
||||||
|
app,
|
||||||
|
token,
|
||||||
|
user,
|
||||||
|
account,
|
||||||
|
)
|
||||||
|
|
||||||
// setup
|
// We should have OK from
|
||||||
recorder := httptest.NewRecorder()
|
// our call to the function.
|
||||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
suite.Equal(http.StatusOK, recorder.Code)
|
||||||
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.Request.Header.Set("accept", "application/json")
|
|
||||||
|
|
||||||
// normally the router would populate these params from the path values,
|
// Target status should now
|
||||||
// but because we're calling the function directly, we need to set them manually.
|
// be "favourited" by us.
|
||||||
ctx.Params = gin.Params{
|
suite.Equal(`{
|
||||||
gin.Param{
|
"account": "yeah this is my account, what about it punk",
|
||||||
Key: statuses.IDKey,
|
"application": {
|
||||||
Value: targetStatus.ID,
|
"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)
|
||||||
}
|
}
|
||||||
|
|
||||||
suite.statusModule.StatusFavePOSTHandler(ctx)
|
// Try to fave a status
|
||||||
|
// that's not faveable by us.
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to fave a status that's not faveable
|
|
||||||
func (suite *StatusFaveTestSuite) TestPostUnfaveable() {
|
func (suite *StatusFaveTestSuite) TestPostUnfaveable() {
|
||||||
t := suite.testTokens["admin_account"]
|
var (
|
||||||
oauthToken := oauth.DBTokenToToken(t)
|
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
|
// We should have 403 from
|
||||||
recorder := httptest.NewRecorder()
|
// our call to the function.
|
||||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
suite.Equal(http.StatusForbidden, recorder.Code)
|
||||||
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")
|
|
||||||
|
|
||||||
// normally the router would populate these params from the path values,
|
// We should get a helpful error.
|
||||||
// but because we're calling the function directly, we need to set them manually.
|
suite.Equal(`{
|
||||||
ctx.Params = gin.Params{
|
"error": "Forbidden: you do not have permission to fave this status"
|
||||||
gin.Param{
|
}`, out)
|
||||||
Key: statuses.IDKey,
|
|
||||||
Value: targetStatus.ID,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suite.statusModule.StatusFavePOSTHandler(ctx)
|
// 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"]
|
||||||
|
)
|
||||||
|
|
||||||
// check response
|
out, recorder := suite.postStatusFave(
|
||||||
suite.EqualValues(http.StatusForbidden, recorder.Code)
|
targetStatus.ID,
|
||||||
|
app,
|
||||||
|
token,
|
||||||
|
user,
|
||||||
|
account,
|
||||||
|
)
|
||||||
|
|
||||||
result := recorder.Result()
|
// We should have OK from
|
||||||
defer result.Body.Close()
|
// our call to the function.
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
suite.Equal(http.StatusOK, recorder.Code)
|
||||||
assert.NoError(suite.T(), err)
|
|
||||||
assert.Equal(suite.T(), `{"error":"Forbidden: you do not have permission to fave this status"}`, string(b))
|
// 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)
|
||||||
|
|
||||||
|
// 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) {
|
func TestStatusFaveTestSuite(t *testing.T) {
|
||||||
|
|
|
@ -223,7 +223,7 @@ func NewProcessor(
|
||||||
processor.tags = tags.New(state, converter)
|
processor.tags = tags.New(state, converter)
|
||||||
processor.timeline = timeline.New(state, converter, visFilter)
|
processor.timeline = timeline.New(state, converter, visFilter)
|
||||||
processor.search = search.New(state, federator, 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)
|
processor.user = user.New(state, converter, oauthServer, emailSender)
|
||||||
|
|
||||||
// The advanced migrations processor sequences advanced migrations from all other processors.
|
// 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/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BoostCreate processes the boost/reblog of target
|
// BoostCreate processes the boost/reblog of target
|
||||||
|
@ -138,6 +139,23 @@ func (p *Processor) BoostCreate(
|
||||||
Target: target.Account,
|
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)
|
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)
|
return p.c.GetAPIStatus(ctx, requester, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *Processor) getFaveableStatus(
|
func (p *Processor) getFaveableStatus(
|
||||||
|
@ -138,8 +139,6 @@ func (p *Processor) FaveCreate(
|
||||||
pendingApproval = false
|
pendingApproval = false
|
||||||
}
|
}
|
||||||
|
|
||||||
status.PendingApproval = &pendingApproval
|
|
||||||
|
|
||||||
// Create a new fave, marking it
|
// Create a new fave, marking it
|
||||||
// as pending approval if necessary.
|
// as pending approval if necessary.
|
||||||
faveID := id.NewULID()
|
faveID := id.NewULID()
|
||||||
|
@ -157,7 +156,7 @@ func (p *Processor) FaveCreate(
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.state.DB.PutStatusFave(ctx, gtsFave); err != nil {
|
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)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,6 +169,23 @@ func (p *Processor) FaveCreate(
|
||||||
Target: status.Account,
|
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)
|
return p.c.GetAPIStatus(ctx, requester, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
|
"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/polls"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/text"
|
"github.com/superseriousbusiness/gotosocial/internal/text"
|
||||||
|
@ -43,6 +44,7 @@ type Processor struct {
|
||||||
|
|
||||||
// other processors
|
// other processors
|
||||||
polls *polls.Processor
|
polls *polls.Processor
|
||||||
|
intReqs *interactionrequests.Processor
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new status processor.
|
// New returns a new status processor.
|
||||||
|
@ -50,6 +52,7 @@ func New(
|
||||||
state *state.State,
|
state *state.State,
|
||||||
common *common.Processor,
|
common *common.Processor,
|
||||||
polls *polls.Processor,
|
polls *polls.Processor,
|
||||||
|
intReqs *interactionrequests.Processor,
|
||||||
federator *federation.Federator,
|
federator *federation.Federator,
|
||||||
converter *typeutils.Converter,
|
converter *typeutils.Converter,
|
||||||
visFilter *visibility.Filter,
|
visFilter *visibility.Filter,
|
||||||
|
@ -66,5 +69,6 @@ func New(
|
||||||
formatter: text.NewFormatter(state.DB),
|
formatter: text.NewFormatter(state.DB),
|
||||||
parseMention: parseMention,
|
parseMention: parseMention,
|
||||||
polls: polls,
|
polls: polls,
|
||||||
|
intReqs: intReqs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
|
"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/polls"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/status"
|
"github.com/superseriousbusiness/gotosocial/internal/processing/status"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"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)
|
common := common.New(&suite.state, suite.mediaManager, suite.typeConverter, suite.federator, visFilter)
|
||||||
polls := polls.New(&common, &suite.state, suite.typeConverter)
|
polls := polls.New(&common, &suite.state, suite.typeConverter)
|
||||||
|
intReqs := interactionrequests.New(&common, &suite.state, suite.typeConverter)
|
||||||
|
|
||||||
suite.status = status.New(
|
suite.status = status.New(
|
||||||
&suite.state,
|
&suite.state,
|
||||||
&common,
|
&common,
|
||||||
&polls,
|
&polls,
|
||||||
|
&intReqs,
|
||||||
suite.federator,
|
suite.federator,
|
||||||
suite.typeConverter,
|
suite.typeConverter,
|
||||||
visFilter,
|
visFilter,
|
||||||
|
|
|
@ -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
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// StatusToAPIStatus converts a gts model status into its api
|
// StatusToAPIStatus converts a gts model
|
||||||
// (frontend) representation for serialization on the API.
|
// status into its api (frontend) representation
|
||||||
|
// for serialization on the API.
|
||||||
//
|
//
|
||||||
// Requesting account can be nil.
|
// 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;
|
// If there is a matching "hide" filter, the returned
|
||||||
// callers need to handle that case by excluding it from results.
|
// status will be nil with a ErrHideStatus error; callers
|
||||||
|
// need to handle that case by excluding it from results.
|
||||||
func (c *Converter) StatusToAPIStatus(
|
func (c *Converter) StatusToAPIStatus(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
s *gtsmodel.Status,
|
status *gtsmodel.Status,
|
||||||
requestingAccount *gtsmodel.Account,
|
requestingAccount *gtsmodel.Account,
|
||||||
filterContext statusfilter.FilterContext,
|
filterContext statusfilter.FilterContext,
|
||||||
filters []*gtsmodel.Filter,
|
filters []*gtsmodel.Filter,
|
||||||
mutes *usermute.CompiledUserMuteList,
|
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) {
|
) (*apimodel.Status, error) {
|
||||||
apiStatus, err := c.statusToFrontend(
|
apiStatus, err := c.statusToFrontend(
|
||||||
ctx,
|
ctx,
|
||||||
s,
|
status,
|
||||||
requestingAccount, // Can be nil.
|
requestingAccount, // Can be nil.
|
||||||
filterContext, // Can be empty.
|
filterContext, // Can be empty.
|
||||||
filters,
|
filters,
|
||||||
|
@ -830,7 +859,7 @@ func (c *Converter) StatusToAPIStatus(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert author to API model.
|
// Convert author to API model.
|
||||||
acct, err := c.AccountToAPIAccountPublic(ctx, s.Account)
|
acct, err := c.AccountToAPIAccountPublic(ctx, status.Account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.Newf("error converting status acct: %w", err)
|
return nil, gtserror.Newf("error converting status acct: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -839,23 +868,43 @@ func (c *Converter) StatusToAPIStatus(
|
||||||
// Convert author of boosted
|
// Convert author of boosted
|
||||||
// status (if set) to API model.
|
// status (if set) to API model.
|
||||||
if apiStatus.Reblog != nil {
|
if apiStatus.Reblog != nil {
|
||||||
boostAcct, err := c.AccountToAPIAccountPublic(ctx, s.BoostOfAccount)
|
boostAcct, err := c.AccountToAPIAccountPublic(ctx, status.BoostOfAccount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.Newf("error converting boost acct: %w", err)
|
return nil, gtserror.Newf("error converting boost acct: %w", err)
|
||||||
}
|
}
|
||||||
apiStatus.Reblog.Account = boostAcct
|
apiStatus.Reblog.Account = boostAcct
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize status for API by pruning
|
if placeholdAttachments {
|
||||||
// attachments that were not locally
|
// Normalize status for API by pruning attachments
|
||||||
// stored, replacing them with a helpful
|
// that were not able to be locally stored, and replacing
|
||||||
// message + links to remote.
|
// them with a helpful message + links to remote.
|
||||||
var aside string
|
var attachNote string
|
||||||
aside, apiStatus.MediaAttachments = placeholderAttachments(apiStatus.MediaAttachments)
|
attachNote, apiStatus.MediaAttachments = placeholderAttachments(apiStatus.MediaAttachments)
|
||||||
apiStatus.Content += aside
|
apiStatus.Content += attachNote
|
||||||
|
|
||||||
|
// Do the same for the reblogged status.
|
||||||
if apiStatus.Reblog != nil {
|
if apiStatus.Reblog != nil {
|
||||||
aside, apiStatus.Reblog.MediaAttachments = placeholderAttachments(apiStatus.Reblog.MediaAttachments)
|
attachNote, apiStatus.Reblog.MediaAttachments = placeholderAttachments(apiStatus.Reblog.MediaAttachments)
|
||||||
apiStatus.Reblog.Content += aside
|
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
|
return apiStatus, nil
|
||||||
|
@ -1972,7 +2021,20 @@ func (c *Converter) ReportToAdminAPIReport(ctx context.Context, r *gtsmodel.Repo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, s := range r.Statuses {
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ReportToAdminAPIReport: error converting status with id %s to api status: %w", s.ID, err)
|
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,
|
req.Status,
|
||||||
requestingAcct,
|
requestingAcct,
|
||||||
statusfilter.FilterContextNone,
|
statusfilter.FilterContextNone,
|
||||||
nil,
|
nil, // No filters.
|
||||||
nil,
|
nil, // No mutes.
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := gtserror.Newf("error converting interacted status: %w", err)
|
err := gtserror.Newf("error converting interacted status: %w", err)
|
||||||
|
@ -2619,13 +2681,20 @@ func (c *Converter) InteractionReqToAPIInteractionReq(
|
||||||
|
|
||||||
var reply *apimodel.Status
|
var reply *apimodel.Status
|
||||||
if req.InteractionType == gtsmodel.InteractionReply {
|
if req.InteractionType == gtsmodel.InteractionReply {
|
||||||
reply, err = c.StatusToAPIStatus(
|
reply, err = c.statusToAPIStatus(
|
||||||
ctx,
|
ctx,
|
||||||
req.Reply,
|
req.Status,
|
||||||
requestingAcct,
|
requestingAcct,
|
||||||
statusfilter.FilterContextNone,
|
statusfilter.FilterContextNone,
|
||||||
nil,
|
nil, // No filters.
|
||||||
nil,
|
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 {
|
if err != nil {
|
||||||
err := gtserror.Newf("error converting reply: %w", err)
|
err := gtserror.Newf("error converting reply: %w", err)
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package typeutils_test
|
package typeutils_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -1708,6 +1709,130 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendPartialInteraction
|
||||||
}`, string(b))
|
}`, 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 is pending your approval. You can quickly accept it by liking, boosting or replying to it. You can also accept or reject it 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</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() {
|
func (suite *InternalToFrontendTestSuite) TestVideoAttachmentToFrontend() {
|
||||||
testAttachment := suite.testAttachments["local_account_1_status_4_attachment_2"]
|
testAttachment := suite.testAttachments["local_account_1_status_4_attachment_2"]
|
||||||
apiAttachment, err := suite.typeconverter.AttachmentToAPIAttachment(context.Background(), testAttachment)
|
apiAttachment, err := suite.typeconverter.AttachmentToAPIAttachment(context.Background(), testAttachment)
|
||||||
|
|
|
@ -19,6 +19,7 @@ package typeutils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -30,6 +31,8 @@ import (
|
||||||
"github.com/k3a/html2text"
|
"github.com/k3a/html2text"
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"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/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/language"
|
"github.com/superseriousbusiness/gotosocial/internal/language"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
|
@ -187,6 +190,47 @@ func placeholderAttachments(arr []*apimodel.Attachment) (string, []*apimodel.Att
|
||||||
return text.SanitizeToHTML(note.String()), arr
|
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 is pending your approval. You can quickly accept it by liking, boosting or replying to it. You can also accept or reject it at the following link: `)
|
||||||
|
note.WriteString(`<a href="` + settingsURL + `" `)
|
||||||
|
note.WriteString(`rel="noreferrer noopener" target="_blank">`)
|
||||||
|
note.WriteString(settingsURL)
|
||||||
|
note.WriteString(`</a>.`)
|
||||||
|
note.WriteString(`</i></p>`)
|
||||||
|
|
||||||
|
return text.SanitizeToHTML(note.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
// ContentToContentLanguage tries to
|
// ContentToContentLanguage tries to
|
||||||
// extract a content string and language
|
// extract a content string and language
|
||||||
// tag string from the given intermediary
|
// tag string from the given intermediary
|
||||||
|
|
|
@ -52,10 +52,10 @@ export default function UserRouter() {
|
||||||
<Route path="/emailpassword" component={EmailPassword} />
|
<Route path="/emailpassword" component={EmailPassword} />
|
||||||
<Route path="/migration" component={UserMigration} />
|
<Route path="/migration" component={UserMigration} />
|
||||||
<Route path="/export-import" component={ExportImport} />
|
<Route path="/export-import" component={ExportImport} />
|
||||||
|
<InteractionRequestsRouter />
|
||||||
<Route><Redirect to="/profile" /></Route>
|
<Route><Redirect to="/profile" /></Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
<InteractionRequestsRouter />
|
|
||||||
</Router>
|
</Router>
|
||||||
</BaseUrlContext.Provider>
|
</BaseUrlContext.Provider>
|
||||||
);
|
);
|
||||||
|
@ -73,13 +73,11 @@ function InteractionRequestsRouter() {
|
||||||
return (
|
return (
|
||||||
<BaseUrlContext.Provider value={absBase}>
|
<BaseUrlContext.Provider value={absBase}>
|
||||||
<Router base={thisBase}>
|
<Router base={thisBase}>
|
||||||
<ErrorBoundary>
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/search" component={InteractionRequests} />
|
<Route path="/search" component={InteractionRequests} />
|
||||||
<Route path="/:reqId" component={InteractionRequestDetail} />
|
<Route path="/:reqId" component={InteractionRequestDetail} />
|
||||||
<Route><Redirect to="/search"/></Route>
|
<Route><Redirect to="/search"/></Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</ErrorBoundary>
|
|
||||||
</Router>
|
</Router>
|
||||||
</BaseUrlContext.Provider>
|
</BaseUrlContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue