mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2026-01-02 10:33:16 -06:00
[feature] Allow implicit accept of pending replies
This commit is contained in:
parent
747c251df6
commit
b0cf28da45
15 changed files with 1318 additions and 377 deletions
|
|
@ -800,26 +800,55 @@ func (c *Converter) TagToAPITag(ctx context.Context, t *gtsmodel.Tag, stubHistor
|
|||
}, nil
|
||||
}
|
||||
|
||||
// StatusToAPIStatus converts a gts model status into its api
|
||||
// (frontend) representation for serialization on the API.
|
||||
// StatusToAPIStatus converts a gts model
|
||||
// status into its api (frontend) representation
|
||||
// for serialization on the API.
|
||||
//
|
||||
// Requesting account can be nil.
|
||||
//
|
||||
// Filter context can be the empty string if these statuses are not being filtered.
|
||||
// filterContext can be the empty string
|
||||
// if these statuses are not being filtered.
|
||||
//
|
||||
// If there is a matching "hide" filter, the returned status will be nil with a ErrHideStatus error;
|
||||
// callers need to handle that case by excluding it from results.
|
||||
// If there is a matching "hide" filter, the returned
|
||||
// status will be nil with a ErrHideStatus error; callers
|
||||
// need to handle that case by excluding it from results.
|
||||
func (c *Converter) StatusToAPIStatus(
|
||||
ctx context.Context,
|
||||
s *gtsmodel.Status,
|
||||
status *gtsmodel.Status,
|
||||
requestingAccount *gtsmodel.Account,
|
||||
filterContext statusfilter.FilterContext,
|
||||
filters []*gtsmodel.Filter,
|
||||
mutes *usermute.CompiledUserMuteList,
|
||||
) (*apimodel.Status, error) {
|
||||
return c.statusToAPIStatus(
|
||||
ctx,
|
||||
status,
|
||||
requestingAccount,
|
||||
filterContext,
|
||||
filters,
|
||||
mutes,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
// statusToAPIStatus is the package-internal implementation
|
||||
// of StatusToAPIStatus that lets the caller customize whether
|
||||
// to placehold unknown attachment types, and/or add a note
|
||||
// about the status being pending and requiring approval.
|
||||
func (c *Converter) statusToAPIStatus(
|
||||
ctx context.Context,
|
||||
status *gtsmodel.Status,
|
||||
requestingAccount *gtsmodel.Account,
|
||||
filterContext statusfilter.FilterContext,
|
||||
filters []*gtsmodel.Filter,
|
||||
mutes *usermute.CompiledUserMuteList,
|
||||
placeholdAttachments bool,
|
||||
addPendingNote bool,
|
||||
) (*apimodel.Status, error) {
|
||||
apiStatus, err := c.statusToFrontend(
|
||||
ctx,
|
||||
s,
|
||||
status,
|
||||
requestingAccount, // Can be nil.
|
||||
filterContext, // Can be empty.
|
||||
filters,
|
||||
|
|
@ -830,7 +859,7 @@ func (c *Converter) StatusToAPIStatus(
|
|||
}
|
||||
|
||||
// Convert author to API model.
|
||||
acct, err := c.AccountToAPIAccountPublic(ctx, s.Account)
|
||||
acct, err := c.AccountToAPIAccountPublic(ctx, status.Account)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error converting status acct: %w", err)
|
||||
}
|
||||
|
|
@ -839,23 +868,43 @@ func (c *Converter) StatusToAPIStatus(
|
|||
// Convert author of boosted
|
||||
// status (if set) to API model.
|
||||
if apiStatus.Reblog != nil {
|
||||
boostAcct, err := c.AccountToAPIAccountPublic(ctx, s.BoostOfAccount)
|
||||
boostAcct, err := c.AccountToAPIAccountPublic(ctx, status.BoostOfAccount)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error converting boost acct: %w", err)
|
||||
}
|
||||
apiStatus.Reblog.Account = boostAcct
|
||||
}
|
||||
|
||||
// Normalize status for API by pruning
|
||||
// attachments that were not locally
|
||||
// stored, replacing them with a helpful
|
||||
// message + links to remote.
|
||||
var aside string
|
||||
aside, apiStatus.MediaAttachments = placeholderAttachments(apiStatus.MediaAttachments)
|
||||
apiStatus.Content += aside
|
||||
if apiStatus.Reblog != nil {
|
||||
aside, apiStatus.Reblog.MediaAttachments = placeholderAttachments(apiStatus.Reblog.MediaAttachments)
|
||||
apiStatus.Reblog.Content += aside
|
||||
if placeholdAttachments {
|
||||
// Normalize status for API by pruning attachments
|
||||
// that were not able to be locally stored, and replacing
|
||||
// them with a helpful message + links to remote.
|
||||
var attachNote string
|
||||
attachNote, apiStatus.MediaAttachments = placeholderAttachments(apiStatus.MediaAttachments)
|
||||
apiStatus.Content += attachNote
|
||||
|
||||
// Do the same for the reblogged status.
|
||||
if apiStatus.Reblog != nil {
|
||||
attachNote, apiStatus.Reblog.MediaAttachments = placeholderAttachments(apiStatus.Reblog.MediaAttachments)
|
||||
apiStatus.Reblog.Content += attachNote
|
||||
}
|
||||
}
|
||||
|
||||
if addPendingNote {
|
||||
// If this status is pending approval and
|
||||
// replies to the requester, add a note
|
||||
// about how to approve or reject the reply.
|
||||
pendingApproval := util.PtrOrValue(status.PendingApproval, false)
|
||||
if pendingApproval &&
|
||||
requestingAccount != nil &&
|
||||
requestingAccount.ID == status.InReplyToAccountID {
|
||||
pendingNote, err := c.pendingReplyNote(ctx, status)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error deriving 'pending reply' note: %w", err)
|
||||
}
|
||||
|
||||
apiStatus.Content += pendingNote
|
||||
}
|
||||
}
|
||||
|
||||
return apiStatus, nil
|
||||
|
|
@ -1972,7 +2021,20 @@ func (c *Converter) ReportToAdminAPIReport(ctx context.Context, r *gtsmodel.Repo
|
|||
}
|
||||
}
|
||||
for _, s := range r.Statuses {
|
||||
status, err := c.StatusToAPIStatus(ctx, s, requestingAccount, statusfilter.FilterContextNone, nil, nil)
|
||||
status, err := c.statusToAPIStatus(
|
||||
ctx,
|
||||
s,
|
||||
requestingAccount,
|
||||
statusfilter.FilterContextNone,
|
||||
nil, // No filters.
|
||||
nil, // No mutes.
|
||||
true, // Placehold unknown attachments.
|
||||
|
||||
// Don't add note about
|
||||
// pending, it's not
|
||||
// relevant here.
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ReportToAdminAPIReport: error converting status with id %s to api status: %w", s.ID, err)
|
||||
}
|
||||
|
|
@ -2609,8 +2671,8 @@ func (c *Converter) InteractionReqToAPIInteractionReq(
|
|||
req.Status,
|
||||
requestingAcct,
|
||||
statusfilter.FilterContextNone,
|
||||
nil,
|
||||
nil,
|
||||
nil, // No filters.
|
||||
nil, // No mutes.
|
||||
)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error converting interacted status: %w", err)
|
||||
|
|
@ -2619,13 +2681,20 @@ func (c *Converter) InteractionReqToAPIInteractionReq(
|
|||
|
||||
var reply *apimodel.Status
|
||||
if req.InteractionType == gtsmodel.InteractionReply {
|
||||
reply, err = c.StatusToAPIStatus(
|
||||
reply, err = c.statusToAPIStatus(
|
||||
ctx,
|
||||
req.Reply,
|
||||
req.Status,
|
||||
requestingAcct,
|
||||
statusfilter.FilterContextNone,
|
||||
nil,
|
||||
nil,
|
||||
nil, // No filters.
|
||||
nil, // No mutes.
|
||||
true, // Placehold unknown attachments.
|
||||
|
||||
// Don't add note about pending;
|
||||
// requester already knows it's
|
||||
// pending because they're looking
|
||||
// at the request right now.
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error converting reply: %w", err)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
package typeutils_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
|
@ -1708,6 +1709,130 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendPartialInteraction
|
|||
}`, string(b))
|
||||
}
|
||||
|
||||
func (suite *InternalToFrontendTestSuite) TestStatusToAPIStatusPendingApproval() {
|
||||
var (
|
||||
testStatus = suite.testStatuses["admin_account_status_5"]
|
||||
requestingAccount = suite.testAccounts["local_account_2"]
|
||||
)
|
||||
|
||||
apiStatus, err := suite.typeconverter.StatusToAPIStatus(
|
||||
context.Background(),
|
||||
testStatus,
|
||||
requestingAccount,
|
||||
statusfilter.FilterContextNone,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// We want to see the HTML in
|
||||
// the status so don't escape it.
|
||||
out := new(bytes.Buffer)
|
||||
enc := json.NewEncoder(out)
|
||||
enc.SetIndent("", " ")
|
||||
enc.SetEscapeHTML(false)
|
||||
if err := enc.Encode(apiStatus); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Equal(`{
|
||||
"id": "01J5QVB9VC76NPPRQ207GG4DRZ",
|
||||
"created_at": "2024-02-20T10:41:37.000Z",
|
||||
"in_reply_to_id": "01F8MHC8VWDRBQR0N1BATDDEM5",
|
||||
"in_reply_to_account_id": "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||
"sensitive": false,
|
||||
"spoiler_text": "",
|
||||
"visibility": "unlisted",
|
||||
"language": null,
|
||||
"uri": "http://localhost:8080/users/admin/statuses/01J5QVB9VC76NPPRQ207GG4DRZ",
|
||||
"url": "http://localhost:8080/@admin/statuses/01J5QVB9VC76NPPRQ207GG4DRZ",
|
||||
"replies_count": 0,
|
||||
"reblogs_count": 0,
|
||||
"favourites_count": 0,
|
||||
"favourited": false,
|
||||
"reblogged": false,
|
||||
"muted": false,
|
||||
"bookmarked": false,
|
||||
"pinned": false,
|
||||
"content": "<p>Hi <span class=\"h-card\"><a href=\"http://localhost:8080/@1happyturtle\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>1happyturtle</span></a></span>, can I reply?</p><hr><p><i lang=\"en\">ℹ️ Note from localhost:8080: This reply to your status is pending your approval. You can accept the reply by liking, replying to, or boosting it. You can also accept or reject the reply at the following link: <a href=\"http://localhost:8080/settings/user/interaction_requests/01J5QVXCCEATJYSXM9H6MZT4JR\" rel=\"noreferrer noopener nofollow\" target=\"_blank\">http://localhost:8080/settings/user/interaction_requests/01J5QVXCCEATJYSXM9H6MZT4JR (opens in a new tab)</a>.</i></p>",
|
||||
"reblog": null,
|
||||
"application": {
|
||||
"name": "superseriousbusiness",
|
||||
"website": "https://superserious.business"
|
||||
},
|
||||
"account": {
|
||||
"id": "01F8MH17FWEB39HZJ76B6VXSKF",
|
||||
"username": "admin",
|
||||
"acct": "admin",
|
||||
"display_name": "",
|
||||
"locked": false,
|
||||
"discoverable": true,
|
||||
"bot": false,
|
||||
"created_at": "2022-05-17T13:10:59.000Z",
|
||||
"note": "",
|
||||
"url": "http://localhost:8080/@admin",
|
||||
"avatar": "",
|
||||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.webp",
|
||||
"header_static": "http://localhost:8080/assets/default_header.webp",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2021-10-20T10:41:37.000Z",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true,
|
||||
"roles": [
|
||||
{
|
||||
"id": "admin",
|
||||
"name": "admin",
|
||||
"color": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"media_attachments": [],
|
||||
"mentions": [
|
||||
{
|
||||
"id": "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||
"username": "1happyturtle",
|
||||
"url": "http://localhost:8080/@1happyturtle",
|
||||
"acct": "1happyturtle"
|
||||
}
|
||||
],
|
||||
"tags": [],
|
||||
"emojis": [],
|
||||
"card": null,
|
||||
"poll": null,
|
||||
"text": "Hi @1happyturtle, can I reply?",
|
||||
"interaction_policy": {
|
||||
"can_favourite": {
|
||||
"always": [
|
||||
"public",
|
||||
"me"
|
||||
],
|
||||
"with_approval": []
|
||||
},
|
||||
"can_reply": {
|
||||
"always": [
|
||||
"public",
|
||||
"me"
|
||||
],
|
||||
"with_approval": []
|
||||
},
|
||||
"can_reblog": {
|
||||
"always": [
|
||||
"public",
|
||||
"me"
|
||||
],
|
||||
"with_approval": []
|
||||
}
|
||||
}
|
||||
}
|
||||
`, out.String())
|
||||
}
|
||||
|
||||
func (suite *InternalToFrontendTestSuite) TestVideoAttachmentToFrontend() {
|
||||
testAttachment := suite.testAttachments["local_account_1_status_4_attachment_2"]
|
||||
apiAttachment, err := suite.typeconverter.AttachmentToAPIAttachment(context.Background(), testAttachment)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package typeutils
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/url"
|
||||
|
|
@ -30,6 +31,8 @@ import (
|
|||
"github.com/k3a/html2text"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/language"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
|
|
@ -187,6 +190,47 @@ func placeholderAttachments(arr []*apimodel.Attachment) (string, []*apimodel.Att
|
|||
return text.SanitizeToHTML(note.String()), arr
|
||||
}
|
||||
|
||||
func (c *Converter) pendingReplyNote(
|
||||
ctx context.Context,
|
||||
s *gtsmodel.Status,
|
||||
) (string, error) {
|
||||
intReq, err := c.state.DB.GetInteractionRequestByInteractionURI(ctx, s.URI)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
// Something's gone wrong.
|
||||
err := gtserror.Newf("db error getting interaction request for %s: %w", s.URI, err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// No interaction request present
|
||||
// for this status. Race condition?
|
||||
if intReq == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var (
|
||||
proto = config.GetProtocol()
|
||||
host = config.GetHost()
|
||||
|
||||
// Build the settings panel URL at which the user
|
||||
// can view + approve/reject the interaction request.
|
||||
//
|
||||
// Eg., https://example.org/settings/user/interaction_requests/01J5QVXCCEATJYSXM9H6MZT4JR
|
||||
settingsURL = proto + "://" + host + "/settings/user/interaction_requests/" + intReq.ID
|
||||
)
|
||||
|
||||
var note strings.Builder
|
||||
note.WriteString(`<hr>`)
|
||||
note.WriteString(`<p><i lang="en">ℹ️ Note from ` + host + `: `)
|
||||
note.WriteString(`This reply to your status is pending your approval. You can accept the reply by liking, replying to, or boosting it. You can also accept or reject the reply at the following link: `)
|
||||
note.WriteString(`<a href="` + settingsURL + `" `)
|
||||
note.WriteString(`rel="noreferrer noopener" target="_blank">`)
|
||||
note.WriteString(settingsURL + ` (opens in a new tab)`)
|
||||
note.WriteString(`</a>.`)
|
||||
note.WriteString(`</i></p>`)
|
||||
|
||||
return text.SanitizeToHTML(note.String()), nil
|
||||
}
|
||||
|
||||
// ContentToContentLanguage tries to
|
||||
// extract a content string and language
|
||||
// tag string from the given intermediary
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue