summaryrefslogtreecommitdiff
path: root/internal/typeutils
diff options
context:
space:
mode:
Diffstat (limited to 'internal/typeutils')
-rw-r--r--internal/typeutils/internaltofrontend.go121
-rw-r--r--internal/typeutils/internaltofrontend_test.go125
-rw-r--r--internal/typeutils/util.go44
3 files changed, 264 insertions, 26 deletions
diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go
index fe49766fa..f36175eab 100644
--- a/internal/typeutils/internaltofrontend.go
+++ b/internal/typeutils/internaltofrontend.go
@@ -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)
diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go
index a44afe67e..dbb6d6a5d 100644
--- a/internal/typeutils/internaltofrontend_test.go
+++ b/internal/typeutils/internaltofrontend_test.go
@@ -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 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() {
testAttachment := suite.testAttachments["local_account_1_status_4_attachment_2"]
apiAttachment, err := suite.typeconverter.AttachmentToAPIAttachment(context.Background(), testAttachment)
diff --git a/internal/typeutils/util.go b/internal/typeutils/util.go
index 3a867ba35..1747dbdcd 100644
--- a/internal/typeutils/util.go
+++ b/internal/typeutils/util.go
@@ -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 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
// extract a content string and language
// tag string from the given intermediary