[feature] Send out and serve polite LikeRequest, ReplyRequest, and AnnounceRequest objects (#4642)
# Description > If this is a code change, please include a summary of what you've coded, and link to the issue(s) it closes/implements. > > If this is a documentation change, please briefly describe what you've changed and why. This pull request updates a bunch of our logic to send out polite LikeRequests, ReplyRequests, and AnnounceRequests where appropriate, instead of sending out "impolite" Likes, Create.Notes, and Announces. GtS v0.20.0 and above should be compatible with these changes, but it will break compatibility for versions of GtS below that version, which do not understand LikeRequest, ReplyRequest, and AnnounceRequest (unfortunately!). Not much to be done about that, unfortunately! This is part of the process of bringing GtS's way of doing interactions in line with the stuff Mastodon introduced in their QuoteRequest stuff, and paves the way to us also doing QuoteRequests at some point. ## Checklist Please put an x inside each checkbox to indicate that you've read and followed it: `[ ]` -> `[x]` If this is a documentation change, only the first two checkboxes must be filled (you can delete the others if you want). - [x] I/we have read the [GoToSocial contribution guidelines](https://codeberg.org/superseriousbusiness/gotosocial/src/branch/main/CONTRIBUTING.md). - [x] I/we have not used so-called 'AI' to create the proposed changes. - [x] I/we have discussed the proposed changes already, either in an issue on the repository, or in the Matrix chat. - [x] I/we have performed a self-review of added code. - [x] I/we have written code that is legible and maintainable by others. - [x] I/we have commented the added code, particularly in hard-to-understand areas. - [ ] I/we have made any necessary changes to documentation. <-- going to do this in a separate PR - [x] I/we have added tests that cover new code. - [x] I/we have run tests and they pass locally with the changes. - [x] I/we have run `go fmt ./...` and `golangci-lint run`. Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4642 Co-authored-by: tobi <tobi.smethurst@protonmail.com> Co-committed-by: tobi <tobi.smethurst@protonmail.com>
This commit is contained in:
@@ -9,7 +9,7 @@ replace modernc.org/sqlite => gitlab.com/NyaaaWhatsUpDoc/sqlite v1.44.3-concurre
|
||||
replace github.com/gin-gonic/gin => codeberg.org/superseriousbusiness/gin v1.11.0-array-binding-fix-2
|
||||
|
||||
require (
|
||||
code.superseriousbusiness.org/activity v1.17.0
|
||||
code.superseriousbusiness.org/activity v1.18.0
|
||||
code.superseriousbusiness.org/exif-terminator v0.11.0
|
||||
code.superseriousbusiness.org/gopkg v0.0.0-20260117214252-d095ed821f5a
|
||||
code.superseriousbusiness.org/httpsig v1.5.0
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
code.superseriousbusiness.org/activity v1.17.0 h1:01x4LyvL5fzKgtce+o3mqYbR1O+RaL6j/z7v/B6ivqo=
|
||||
code.superseriousbusiness.org/activity v1.17.0/go.mod h1:BTMWJIAuwDH1w+ieRP5N+T5LipbXjw35U6KZy0V/xdg=
|
||||
code.superseriousbusiness.org/activity v1.18.0 h1:TPxRQa7VVlA8Nvu8/cumfoOF9VpyoiYkOqa51OB6wwY=
|
||||
code.superseriousbusiness.org/activity v1.18.0/go.mod h1:BTMWJIAuwDH1w+ieRP5N+T5LipbXjw35U6KZy0V/xdg=
|
||||
code.superseriousbusiness.org/exif-terminator v0.11.0 h1:Hof0MCcsa+1fS17gf86fTTZ8AQnMY9h9kzcc+2C6mVg=
|
||||
code.superseriousbusiness.org/exif-terminator v0.11.0/go.mod h1:9sutT1axa/kSdlPLlRFjCNKmyo/KNx8eX3XZvWBlAEY=
|
||||
code.superseriousbusiness.org/go-jpeg-image-structure/v2 v2.3.0 h1:r9uq8StaSHYKJ8DklR9Xy+E9c40G1Z8yj5TRGi8L6+4=
|
||||
|
||||
+78
-22
@@ -25,6 +25,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -82,41 +83,96 @@ func ExtractInstruments(with WithInstrument) []TypeOrIRI {
|
||||
return instrs
|
||||
}
|
||||
|
||||
// ExtractActivityData will extract the usable data type (e.g. Note, Question, etc) and corresponding JSON, from activity.
|
||||
func ExtractActivityData(activity pub.Activity, rawJSON map[string]any) ([]TypeOrIRI, []any, bool) {
|
||||
// ExtractActivityObjectsAndInstruments will extract the usable
|
||||
// data type(s) (e.g. Note, Question, etc) and corresponding raw
|
||||
// JSON(s), from the `object` and `instrument` field of given activity.
|
||||
func ExtractActivityObjectsAndInstruments(
|
||||
activity pub.Activity,
|
||||
rawJSON map[string]any,
|
||||
) ([]TypeOrIRI, []any) {
|
||||
switch typeName := activity.GetTypeName(); {
|
||||
// Activity (has "object").
|
||||
// Activity: has "object"
|
||||
// and/or "instrument".
|
||||
case isActivity(typeName):
|
||||
objTypes := ExtractObjects(activity)
|
||||
if len(objTypes) == 0 {
|
||||
return nil, nil, false
|
||||
}
|
||||
// Gather object types and raw json.
|
||||
objTypes, objJSON := extractObjectTypesAndJSON(activity, rawJSON)
|
||||
|
||||
var objJSON []any
|
||||
switch json := rawJSON["object"].(type) {
|
||||
case nil:
|
||||
// do nothing
|
||||
case map[string]any:
|
||||
// Wrap map in slice.
|
||||
objJSON = []any{json}
|
||||
case []any:
|
||||
// Use existing slice.
|
||||
objJSON = json
|
||||
}
|
||||
// Gather instrument types and raw json.
|
||||
instTypes, instJSON := extractInstrumentTypesAndJSON(activity, rawJSON)
|
||||
|
||||
return objTypes, objJSON, true
|
||||
// Return concatenated slices for further processing.
|
||||
return slices.Concat(objTypes, instTypes), slices.Concat(objJSON, instJSON)
|
||||
|
||||
// IntransitiveAcitivity (no "object").
|
||||
// IntransitiveActivity: no
|
||||
// "object" or "instrument".
|
||||
case isIntransitiveActivity(typeName):
|
||||
asTypeOrIRI := _TypeOrIRI{activity} // wrap activity.
|
||||
return []TypeOrIRI{&asTypeOrIRI}, []any{rawJSON}, true
|
||||
return []TypeOrIRI{&asTypeOrIRI}, []any{rawJSON}
|
||||
|
||||
// Unknown.
|
||||
default:
|
||||
return nil, nil, false
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// extractObjectTypesAndJSON is a utility
|
||||
// function to get objects and their correspnding
|
||||
// raw json from the given activity.
|
||||
func extractObjectTypesAndJSON(
|
||||
activity pub.Activity,
|
||||
rawJSON map[string]any,
|
||||
) ([]TypeOrIRI, []any) {
|
||||
objTypes := ExtractObjects(activity)
|
||||
if len(objTypes) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Look for corresponding
|
||||
// objects in the JSON.
|
||||
var objJSON []any
|
||||
switch json := rawJSON["object"].(type) {
|
||||
case nil:
|
||||
// do nothing
|
||||
case map[string]any:
|
||||
// Wrap map in slice.
|
||||
objJSON = []any{json}
|
||||
case []any:
|
||||
// Use existing slice.
|
||||
objJSON = json
|
||||
}
|
||||
|
||||
return objTypes, objJSON
|
||||
}
|
||||
|
||||
// extractInstrumentTypesAndJSON is a utility
|
||||
// function to get instruments and their correspnding
|
||||
// raw json from the given activity.
|
||||
func extractInstrumentTypesAndJSON(
|
||||
activity pub.Activity,
|
||||
rawJSON map[string]any,
|
||||
) ([]TypeOrIRI, []any) {
|
||||
instTypes := ExtractInstruments(activity)
|
||||
if len(instTypes) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Look for corresponding
|
||||
// instruments in the JSON.
|
||||
var instJSON []any
|
||||
switch json := rawJSON["instrument"].(type) {
|
||||
case nil:
|
||||
// do nothing
|
||||
case map[string]any:
|
||||
// Wrap map in slice.
|
||||
instJSON = []any{json}
|
||||
case []any:
|
||||
// Use existing slice.
|
||||
instJSON = json
|
||||
}
|
||||
|
||||
return instTypes, instJSON
|
||||
}
|
||||
|
||||
// ExtractAccountables extracts Accountable objects from a slice TypeOrIRI, returning extracted and remaining TypeOrIRIs.
|
||||
func ExtractAccountables(arr []TypeOrIRI) ([]Accountable, []TypeOrIRI) {
|
||||
var accounts []Accountable
|
||||
|
||||
@@ -232,6 +232,7 @@ type Activityable interface {
|
||||
WithActor
|
||||
WithObject
|
||||
WithPublished
|
||||
WithInstrument
|
||||
}
|
||||
|
||||
// Accountable represents the minimum activitypub interface for representing an 'account'.
|
||||
@@ -424,6 +425,7 @@ type InteractionRequestable interface {
|
||||
vocab.Type
|
||||
|
||||
WithActor
|
||||
WithTo
|
||||
WithObject
|
||||
WithInstrument
|
||||
}
|
||||
|
||||
+101
-8
@@ -33,15 +33,19 @@ import (
|
||||
another instance to dereference something.
|
||||
*/
|
||||
|
||||
// NormalizeIncomingActivityObject normalizes the 'object'.'content' field of the given Activity.
|
||||
// NormalizeIncomingActivity normalizes any
|
||||
// Statusables and Accountables in the 'object' and
|
||||
// 'instrument' fields of the given Activity.
|
||||
//
|
||||
// The rawActivity map should the freshly deserialized json representation of the Activity.
|
||||
//
|
||||
// This function is a noop if the type passed in is anything except a Create or Update with a Statusable or Accountable as its Object.
|
||||
func NormalizeIncomingActivity(activity pub.Activity, rawJSON map[string]interface{}) {
|
||||
// From the activity extract the data vocab.Type + its "raw" JSON.
|
||||
dataIfaces, rawData, ok := ExtractActivityData(activity, rawJSON)
|
||||
if !ok || len(dataIfaces) != len(rawData) {
|
||||
// The rawJSON map should be the freshly
|
||||
// deserialized json representation of the Activity.
|
||||
func NormalizeIncomingActivity(
|
||||
activity pub.Activity,
|
||||
rawJSON map[string]any,
|
||||
) {
|
||||
// From the activity extract the data vocab.Types + "rawData" JSON.
|
||||
dataIfaces, rawData := ExtractActivityObjectsAndInstruments(activity, rawJSON)
|
||||
if len(dataIfaces) != len(rawData) {
|
||||
// non-equal lengths *shouldn't* happen,
|
||||
// but this is just an integrity check.
|
||||
return
|
||||
@@ -767,6 +771,95 @@ func NormalizeOutgoingObjectProp(item WithObject, rawJSON map[string]interface{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NormalizeOutgoingInstrumentProp normalizes each Instrument entry in the rawJSON of the given
|
||||
// item by calling custom serialization / normalization functions on them in turn.
|
||||
//
|
||||
// This function also unnests single-entry arrays, so that:
|
||||
//
|
||||
// "instrument": [
|
||||
// {
|
||||
// ...
|
||||
// }
|
||||
// ]
|
||||
//
|
||||
// Becomes:
|
||||
//
|
||||
// "instrument": {
|
||||
// ...
|
||||
// }
|
||||
//
|
||||
// Noop for each Instrument entry that isn't an Accountable or Statusable.
|
||||
func NormalizeOutgoingInstrumentProp(item WithInstrument, rawJSON map[string]interface{}) error {
|
||||
instrumentProp := item.GetActivityStreamsInstrument()
|
||||
if instrumentProp == nil {
|
||||
// Nothing to do,
|
||||
// bail early.
|
||||
return nil
|
||||
}
|
||||
|
||||
instrumentPropLen := instrumentProp.Len()
|
||||
if instrumentPropLen == 0 {
|
||||
// Nothing to do,
|
||||
// bail early.
|
||||
return nil
|
||||
}
|
||||
|
||||
// The thing we already serialized has instruments
|
||||
// on it, so we should see if we need to custom
|
||||
// serialize any of those instruments, and replace
|
||||
// them on the data map as necessary.
|
||||
instruments := make([]interface{}, 0, instrumentPropLen)
|
||||
for iter := instrumentProp.Begin(); iter != instrumentProp.End(); iter = iter.Next() {
|
||||
if iter.IsIRI() {
|
||||
// Plain IRIs don't need custom serialization.
|
||||
instruments = append(instruments, iter.GetIRI().String())
|
||||
continue
|
||||
}
|
||||
|
||||
var (
|
||||
instrumentType = iter.GetType()
|
||||
instrumentSer map[string]interface{}
|
||||
)
|
||||
|
||||
if instrumentType == nil {
|
||||
// This is awkward.
|
||||
return gtserror.Newf("could not resolve instrument iter %T to vocab.Type", iter)
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
// In the below statusable serialization,
|
||||
// `@context` will be included in the wrapping
|
||||
// type already, so we shouldn't also include
|
||||
// it in the instrument itself.
|
||||
switch tn := instrumentType.GetTypeName(); {
|
||||
case IsStatusable(tn):
|
||||
// IsStatusable includes Pollable as well.
|
||||
instrumentSer, err = serializeStatusable(instrumentType, false)
|
||||
|
||||
default:
|
||||
// No custom serializer for this type; serialize as normal.
|
||||
instrumentSer, err = instrumentType.Serialize()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
instruments = append(instruments, instrumentSer)
|
||||
}
|
||||
|
||||
if instrumentPropLen == 1 {
|
||||
// Unnest single instrument.
|
||||
rawJSON["instrument"] = instruments[0]
|
||||
} else {
|
||||
// Array of instruments.
|
||||
rawJSON["instrument"] = instruments
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NormalizeOutgoingOneOrAnyOfProp replaces single-entry oneOf or anyOf values
|
||||
// with single-entry arrays, for better compatibility with other AP implementations.
|
||||
//
|
||||
|
||||
@@ -20,6 +20,7 @@ package ap_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.superseriousbusiness.org/activity/streams"
|
||||
"code.superseriousbusiness.org/activity/streams/vocab"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/ap"
|
||||
"code.superseriousbusiness.org/gotosocial/testrig"
|
||||
@@ -258,6 +259,38 @@ func (suite *NormalizeTestSuite) TestNormalizeActivityObject() {
|
||||
)
|
||||
}
|
||||
|
||||
func (suite *NormalizeTestSuite) TestNormalizeActivityInstrument() {
|
||||
note, raw := suite.getStatusable()
|
||||
content := ap.ExtractContent(note)
|
||||
suite.Equal(
|
||||
`update: As of this morning there are now more than 7 million Mastodon users, most from the <a class="hashtag" data-tag="twittermigration" href="https://example.org/tag/twittermigration" rel="tag ugc">#TwitterMigration%3C/a%3E.%3Cbr%3E%3Cbr%3EIn%20fact,%20100,000%20new%20accounts%20have%20been%20created%20since%20last%20night.%3Cbr%3E%3Cbr%3ESince%20last%20night&%2339;s%20spike%208,000-12,000%20new%20accounts%20are%20being%20created%20every%20hour.%3Cbr%3E%3Cbr%3EYesterday,%20I%20estimated%20that%20Mastodon%20would%20have%208%20million%20users%20by%20the%20end%20of%20the%20week.%20That%20might%20happen%20a%20lot%20sooner%20if%20this%20trend%20continues.`,
|
||||
content.Content,
|
||||
)
|
||||
|
||||
// Malformed contentMap entry
|
||||
// will not be extractable yet.
|
||||
suite.Empty(content.ContentMap["en"])
|
||||
|
||||
replyRequest := streams.NewGoToSocialReplyRequest()
|
||||
instProp := streams.NewActivityStreamsInstrumentProperty()
|
||||
instProp.AppendActivityStreamsNote(note)
|
||||
replyRequest.SetActivityStreamsInstrument(instProp)
|
||||
|
||||
ap.NormalizeIncomingActivity(replyRequest, map[string]interface{}{"instrument": raw})
|
||||
content = ap.ExtractContent(note)
|
||||
|
||||
suite.Equal(
|
||||
`UPDATE: As of this morning there are now more than 7 million Mastodon users, most from the <a class="hashtag" href="https://example.org/tag/twittermigration" rel="tag ugc nofollow noreferrer noopener" target="_blank">#TwitterMigration</a>.<br><br>In fact, 100,000 new accounts have been created since last night.<br><br>Since last night's spike 8,000-12,000 new accounts are being created every hour.<br><br>Yesterday, I estimated that Mastodon would have 8 million users by the end of the week. That might happen a lot sooner if this trend continues.`,
|
||||
content.Content,
|
||||
)
|
||||
|
||||
// Content map entry should now be extractable.
|
||||
suite.Equal(
|
||||
`UPDATE: As of this morning there are now more than 7 million Mastodon users, most from the <a class="hashtag" href="https://example.org/tag/twittermigration" rel="tag ugc nofollow noreferrer noopener" target="_blank">#TwitterMigration</a>.<br><br>In fact, 100,000 new accounts have been created since last night.<br><br>Since last night's spike 8,000-12,000 new accounts are being created every hour.<br><br>Yesterday, I estimated that Mastodon would have 8 million users by the end of the week. That might happen a lot sooner if this trend continues.`,
|
||||
content.ContentMap["en"],
|
||||
)
|
||||
}
|
||||
|
||||
func (suite *NormalizeTestSuite) TestNormalizeStatusableAttachmentsOneAttachment() {
|
||||
note, raw := suite.getStatusableWithOneAttachment()
|
||||
|
||||
|
||||
@@ -157,6 +157,10 @@ func serializeActivityable(t vocab.Type, includeContext bool) (map[string]interf
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := NormalizeOutgoingInstrumentProp(activityable, data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,7 @@
|
||||
package emoji
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
|
||||
@@ -28,10 +26,9 @@ import (
|
||||
)
|
||||
|
||||
func (m *Module) EmojiGetHandler(c *gin.Context) {
|
||||
emojiID := strings.ToUpper(c.Param(apiutil.IDKey))
|
||||
if emojiID == "" {
|
||||
err := errors.New("no emoji id specified in request")
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
emojiID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -21,31 +21,24 @@ import (
|
||||
"net/http"
|
||||
|
||||
apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AcceptGETHandler serves an interaction request as an ActivityStreams Accept.
|
||||
func (m *Module) AcceptGETHandler(c *gin.Context) {
|
||||
username, errWithCode := apiutil.ParseUsername(c.Param(apiutil.UsernameKey))
|
||||
username, id, contentType, errWithCode := m.parseCommonWithID(c)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
reqID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
if contentType == apiutil.TextHTML {
|
||||
// Redirect to account web view.
|
||||
c.Redirect(http.StatusSeeOther, "/@"+username)
|
||||
return
|
||||
}
|
||||
|
||||
contentType, err := apiutil.NegotiateAccept(c, apiutil.ActivityPubHeaders...)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.Fedi().AcceptGet(c.Request.Context(), username, reqID)
|
||||
resp, errWithCode := m.processor.Fedi().AcceptGet(c.Request.Context(), username, id)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
|
||||
@@ -21,32 +21,25 @@ import (
|
||||
"net/http"
|
||||
|
||||
apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AuthorizationGETHandler serves an accepted interaction request as a
|
||||
// LikeAuthorization, ReplyAuthorization, or AnnounceAuthorization type.
|
||||
func (m *Module) AuthorizationGETHandler(c *gin.Context) {
|
||||
username, errWithCode := apiutil.ParseUsername(c.Param(apiutil.UsernameKey))
|
||||
username, id, contentType, errWithCode := m.parseCommonWithID(c)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
reqID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
if contentType == apiutil.TextHTML {
|
||||
// Redirect to account web view.
|
||||
c.Redirect(http.StatusSeeOther, "/@"+username)
|
||||
return
|
||||
}
|
||||
|
||||
contentType, err := apiutil.NegotiateAccept(c, apiutil.ActivityPubHeaders...)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.Fedi().AuthorizationGet(c.Request.Context(), username, reqID)
|
||||
resp, errWithCode := m.processor.Fedi().AuthorizationGet(c.Request.Context(), username, id)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
|
||||
@@ -17,6 +17,12 @@
|
||||
|
||||
package users
|
||||
|
||||
import (
|
||||
apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// SwaggerCollection represents an ActivityPub Collection.
|
||||
// swagger:model swaggerCollection
|
||||
type SwaggerCollection struct {
|
||||
@@ -78,3 +84,46 @@ type SwaggerFeaturedCollection struct {
|
||||
// example: 2
|
||||
TotalItems int
|
||||
}
|
||||
|
||||
func (m *Module) parseCommon(c *gin.Context) (
|
||||
username string,
|
||||
contentType string,
|
||||
errWithCode gtserror.WithCode,
|
||||
) {
|
||||
// Get username from request params.
|
||||
username, errWithCode = apiutil.ParseUsername(c.Param(apiutil.UsernameKey))
|
||||
if errWithCode != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Get content type.
|
||||
var err error
|
||||
contentType, err = apiutil.NegotiateAccept(c, apiutil.ActivityPubOrHTMLHeaders...)
|
||||
if err != nil {
|
||||
errWithCode = gtserror.NewErrorNotAcceptable(err, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Module) parseCommonWithID(c *gin.Context) (
|
||||
username string,
|
||||
id string,
|
||||
contentType string,
|
||||
errWithCode gtserror.WithCode,
|
||||
) {
|
||||
// Do parsecommon to get username + content type.
|
||||
username, contentType, errWithCode = m.parseCommon(c)
|
||||
if errWithCode != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Additionally get ID from request params.
|
||||
id, errWithCode = apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -18,12 +18,9 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -74,28 +71,19 @@ import (
|
||||
// "$ref": "#/definitions/error"
|
||||
// description: not found
|
||||
func (m *Module) FeaturedCollectionGETHandler(c *gin.Context) {
|
||||
// usernames on our instance are always lowercase
|
||||
requestedUser := strings.ToLower(c.Param(apiutil.UsernameKey))
|
||||
if requestedUser == "" {
|
||||
err := errors.New("no username specified in request")
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
username, contentType, errWithCode := m.parseCommon(c)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
contentType, err := apiutil.NegotiateAccept(c, apiutil.ActivityPubOrHTMLHeaders...)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
if contentType == apiutil.TextHTML {
|
||||
// Redirect to account web view.
|
||||
c.Redirect(http.StatusSeeOther, "/@"+username)
|
||||
return
|
||||
}
|
||||
|
||||
if contentType == string(apiutil.TextHTML) {
|
||||
// This isn't an ActivityPub request;
|
||||
// redirect to the user's profile.
|
||||
c.Redirect(http.StatusSeeOther, "/@"+requestedUser)
|
||||
return
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.Fedi().FeaturedCollectionGet(c.Request.Context(), requestedUser)
|
||||
resp, errWithCode := m.processor.Fedi().FeaturedCollectionGet(c.Request.Context(), username)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
|
||||
@@ -18,36 +18,24 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/paging"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// FollowersGETHandler returns a collection of URIs for followers of the target user, formatted so that other AP servers can understand it.
|
||||
func (m *Module) FollowersGETHandler(c *gin.Context) {
|
||||
// usernames on our instance are always lowercase
|
||||
requestedUser := strings.ToLower(c.Param(apiutil.UsernameKey))
|
||||
if requestedUser == "" {
|
||||
err := errors.New("no username specified in request")
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
username, contentType, errWithCode := m.parseCommon(c)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
contentType, err := apiutil.NegotiateAccept(c, apiutil.ActivityPubOrHTMLHeaders...)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if contentType == string(apiutil.TextHTML) {
|
||||
// This isn't an ActivityPub request;
|
||||
// redirect to the user's profile.
|
||||
c.Redirect(http.StatusSeeOther, "/@"+requestedUser)
|
||||
if contentType == apiutil.TextHTML {
|
||||
// Redirect to account web view.
|
||||
c.Redirect(http.StatusSeeOther, "/@"+username)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -61,7 +49,7 @@ func (m *Module) FollowersGETHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.Fedi().FollowersGet(c.Request.Context(), requestedUser, page)
|
||||
resp, errWithCode := m.processor.Fedi().FollowersGet(c.Request.Context(), username, page)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
|
||||
@@ -18,36 +18,24 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/paging"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// FollowingGETHandler returns a collection of URIs for accounts that the target user follows, formatted so that other AP servers can understand it.
|
||||
func (m *Module) FollowingGETHandler(c *gin.Context) {
|
||||
// usernames on our instance are always lowercase
|
||||
requestedUser := strings.ToLower(c.Param(apiutil.UsernameKey))
|
||||
if requestedUser == "" {
|
||||
err := errors.New("no username specified in request")
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
username, contentType, errWithCode := m.parseCommon(c)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
contentType, err := apiutil.NegotiateAccept(c, apiutil.ActivityPubOrHTMLHeaders...)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if contentType == string(apiutil.TextHTML) {
|
||||
// This isn't an ActivityPub request;
|
||||
// redirect to the user's profile.
|
||||
c.Redirect(http.StatusSeeOther, "/@"+requestedUser)
|
||||
if contentType == apiutil.TextHTML {
|
||||
// Redirect to account web view.
|
||||
c.Redirect(http.StatusSeeOther, "/@"+username)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -61,7 +49,7 @@ func (m *Module) FollowingGETHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.Fedi().FollowingGet(c.Request.Context(), requestedUser, page)
|
||||
resp, errWithCode := m.processor.Fedi().FollowingGet(c.Request.Context(), username, page)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// 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 users
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (m *Module) LikeRequestsGETHandler(c *gin.Context) {
|
||||
username, id, contentType, errWithCode := m.parseCommonWithID(c)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if contentType == apiutil.TextHTML {
|
||||
// Redirect to account web view.
|
||||
c.Redirect(http.StatusSeeOther, "/@"+username)
|
||||
return
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.Fedi().LikeRequestGet(c.Request.Context(), username, id)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSONType(c, http.StatusOK, contentType, resp)
|
||||
}
|
||||
|
||||
func (m *Module) ReplyRequestsGETHandler(c *gin.Context) {
|
||||
username, id, contentType, errWithCode := m.parseCommonWithID(c)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if contentType == apiutil.TextHTML {
|
||||
// Redirect to account web view.
|
||||
c.Redirect(http.StatusSeeOther, "/@"+username)
|
||||
return
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.Fedi().ReplyRequestGet(c.Request.Context(), username, id)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSONType(c, http.StatusOK, contentType, resp)
|
||||
}
|
||||
|
||||
func (m *Module) AnnounceRequestsGETHandler(c *gin.Context) {
|
||||
username, id, contentType, errWithCode := m.parseCommonWithID(c)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if contentType == apiutil.TextHTML {
|
||||
// Redirect to account web view.
|
||||
c.Redirect(http.StatusSeeOther, "/@"+username)
|
||||
return
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.Fedi().AnnounceRequestGet(c.Request.Context(), username, id)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSONType(c, http.StatusOK, contentType, resp)
|
||||
}
|
||||
@@ -18,12 +18,9 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/paging"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -91,24 +88,15 @@ import (
|
||||
// "$ref": "#/definitions/error"
|
||||
// description: not found
|
||||
func (m *Module) OutboxGETHandler(c *gin.Context) {
|
||||
// usernames on our instance are always lowercase
|
||||
requestedUser := strings.ToLower(c.Param(apiutil.UsernameKey))
|
||||
if requestedUser == "" {
|
||||
err := errors.New("no username specified in request")
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
username, contentType, errWithCode := m.parseCommon(c)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
contentType, err := apiutil.NegotiateAccept(c, apiutil.ActivityPubOrHTMLHeaders...)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if contentType == string(apiutil.TextHTML) {
|
||||
// This isn't an ActivityPub request;
|
||||
// redirect to the user's profile.
|
||||
c.Redirect(http.StatusSeeOther, "/@"+requestedUser)
|
||||
if contentType == apiutil.TextHTML {
|
||||
// Redirect to account web view.
|
||||
c.Redirect(http.StatusSeeOther, "/@"+username)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -122,7 +110,7 @@ func (m *Module) OutboxGETHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.Fedi().OutboxGet(c.Request.Context(), requestedUser, page)
|
||||
resp, errWithCode := m.processor.Fedi().OutboxGet(c.Request.Context(), username, page)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
|
||||
@@ -18,12 +18,9 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/paging"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -98,31 +95,15 @@ import (
|
||||
// "$ref": "#/definitions/error"
|
||||
// description: not found
|
||||
func (m *Module) StatusRepliesGETHandler(c *gin.Context) {
|
||||
// usernames on our instance are always lowercase
|
||||
requestedUser := strings.ToLower(c.Param(apiutil.UsernameKey))
|
||||
if requestedUser == "" {
|
||||
err := errors.New("no username specified in request")
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
username, statusID, contentType, errWithCode := m.parseCommonWithID(c)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
// status IDs on our instance are always uppercase
|
||||
requestedStatusID := strings.ToUpper(c.Param(apiutil.IDKey))
|
||||
if requestedStatusID == "" {
|
||||
err := errors.New("no status id specified in request")
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
contentType, err := apiutil.NegotiateAccept(c, apiutil.ActivityPubOrHTMLHeaders...)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if contentType == string(apiutil.TextHTML) {
|
||||
// redirect to the status
|
||||
c.Redirect(http.StatusSeeOther, "/@"+requestedUser+"/statuses/"+requestedStatusID)
|
||||
if contentType == apiutil.TextHTML {
|
||||
// Redirect to status web view.
|
||||
c.Redirect(http.StatusSeeOther, "/@"+username+"/statuses/"+statusID)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -158,8 +139,8 @@ func (m *Module) StatusRepliesGETHandler(c *gin.Context) {
|
||||
// Fetch serialized status replies response for input status.
|
||||
resp, errWithCode := m.processor.Fedi().StatusRepliesGet(
|
||||
c.Request.Context(),
|
||||
requestedUser,
|
||||
requestedStatusID,
|
||||
username,
|
||||
statusID,
|
||||
page,
|
||||
onlyOtherAccounts,
|
||||
)
|
||||
|
||||
@@ -18,46 +18,27 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// StatusGETHandler serves the target status as an activitystreams NOTE so that other AP servers can parse it.
|
||||
func (m *Module) StatusGETHandler(c *gin.Context) {
|
||||
// usernames on our instance are always lowercase
|
||||
requestedUser := strings.ToLower(c.Param(apiutil.UsernameKey))
|
||||
if requestedUser == "" {
|
||||
err := errors.New("no username specified in request")
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
username, statusID, contentType, errWithCode := m.parseCommonWithID(c)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
// status IDs on our instance are always uppercase
|
||||
requestedStatusID := strings.ToUpper(c.Param(apiutil.IDKey))
|
||||
if requestedStatusID == "" {
|
||||
err := errors.New("no status id specified in request")
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
if contentType == apiutil.TextHTML {
|
||||
// Redirect to status web view.
|
||||
c.Redirect(http.StatusSeeOther, "/@"+username+"/statuses/"+statusID)
|
||||
return
|
||||
}
|
||||
|
||||
contentType, err := apiutil.NegotiateAccept(c, apiutil.ActivityPubOrHTMLHeaders...)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if contentType == string(apiutil.TextHTML) {
|
||||
// redirect to the status
|
||||
c.Redirect(http.StatusSeeOther, "/@"+requestedUser+"/statuses/"+requestedStatusID)
|
||||
return
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.Fedi().StatusGet(c.Request.Context(), requestedUser, requestedStatusID)
|
||||
resp, errWithCode := m.processor.Fedi().StatusGet(c.Request.Context(), username, statusID)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
|
||||
@@ -38,6 +38,9 @@ const (
|
||||
StatusRepliesPath = StatusPath + "/replies"
|
||||
AcceptPath = BasePath + "/" + uris.AcceptsPath + "/:" + apiutil.IDKey
|
||||
AuthorizationsPath = BasePath + "/" + uris.AuthorizationsPath + "/:" + apiutil.IDKey
|
||||
LikeRequestsPath = BasePath + "/" + uris.LikeRequestsPath + "/:" + apiutil.IDKey
|
||||
ReplyRequestsPath = BasePath + "/" + uris.ReplyRequestsPath + "/:" + apiutil.IDKey
|
||||
AnnounceRequestsPath = BasePath + "/" + uris.AnnounceRequestsPath + "/:" + apiutil.IDKey
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
@@ -61,4 +64,7 @@ func (m *Module) Route(attachHandler func(method string, path string, f ...gin.H
|
||||
attachHandler(http.MethodGet, OutboxPath, m.OutboxGETHandler)
|
||||
attachHandler(http.MethodGet, AcceptPath, m.AcceptGETHandler)
|
||||
attachHandler(http.MethodGet, AuthorizationsPath, m.AuthorizationGETHandler)
|
||||
attachHandler(http.MethodGet, LikeRequestsPath, m.LikeRequestsGETHandler)
|
||||
attachHandler(http.MethodGet, ReplyRequestsPath, m.ReplyRequestsGETHandler)
|
||||
attachHandler(http.MethodGet, AnnounceRequestsPath, m.AnnounceRequestsGETHandler)
|
||||
}
|
||||
|
||||
@@ -18,12 +18,9 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -37,30 +34,21 @@ import (
|
||||
// And of course, the request should be refused if the account or server making the
|
||||
// request is blocked.
|
||||
func (m *Module) UsersGETHandler(c *gin.Context) {
|
||||
// usernames on our instance are always lowercase
|
||||
requestedUser := strings.ToLower(c.Param(apiutil.UsernameKey))
|
||||
if requestedUser == "" {
|
||||
err := errors.New("no username specified in request")
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
username, contentType, errWithCode := m.parseCommon(c)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
contentType, err := apiutil.NegotiateAccept(c, apiutil.ActivityPubOrHTMLHeaders...)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
// If HTML is requested, redirect
|
||||
// to user's profile instead.
|
||||
if contentType == string(apiutil.TextHTML) {
|
||||
c.Redirect(http.StatusSeeOther, "/@"+requestedUser)
|
||||
if contentType == apiutil.TextHTML {
|
||||
// Redirect to account web view.
|
||||
c.Redirect(http.StatusSeeOther, "/@"+username)
|
||||
return
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.Fedi().UserGet(
|
||||
c.Request.Context(),
|
||||
requestedUser,
|
||||
username,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
|
||||
@@ -63,10 +63,6 @@ const (
|
||||
|
||||
TagNameKey = "tag_name"
|
||||
|
||||
/* Web endpoint keys */
|
||||
|
||||
WebStatusIDKey = "status"
|
||||
|
||||
/* Domain permission keys */
|
||||
|
||||
DomainPermissionExportKey = "export"
|
||||
@@ -247,6 +243,9 @@ func ParseID(value string) (string, gtserror.WithCode) {
|
||||
return "", requiredError(key)
|
||||
}
|
||||
|
||||
// ULIDs are always uppercase.
|
||||
value = strings.ToUpper(value)
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
@@ -287,15 +286,10 @@ func ParseUsername(value string) (string, gtserror.WithCode) {
|
||||
return "", requiredError(key)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func ParseWebStatusID(value string) (string, gtserror.WithCode) {
|
||||
key := WebStatusIDKey
|
||||
|
||||
if value == "" {
|
||||
return "", requiredError(key)
|
||||
}
|
||||
// Usernames on our instance are always lowercase.
|
||||
// TODO: Update this when we allow different cases etc.
|
||||
// See: https://codeberg.org/superseriousbusiness/gotosocial/issues/1813
|
||||
value = strings.ToLower(value)
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ import (
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/id"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/paging"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/typeutils"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/util"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
@@ -57,9 +56,23 @@ func (suite *InteractionTestSuite) markInteractionsPending(
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Put an impolite interaction request in the DB for this reply.
|
||||
req := typeutils.StatusToImpoliteInteractionRequest(reply)
|
||||
if err := suite.state.DB.PutInteractionRequest(ctx, req); err != nil {
|
||||
// Put an interaction request in the DB for this reply.
|
||||
intReqID := id.NewULIDFromTime(reply.CreatedAt)
|
||||
intReq := >smodel.InteractionRequest{
|
||||
ID: intReqID,
|
||||
TargetStatusID: reply.InReplyToID,
|
||||
TargetStatus: reply.InReplyTo,
|
||||
TargetAccountID: reply.InReplyToAccountID,
|
||||
TargetAccount: reply.InReplyToAccount,
|
||||
InteractingAccountID: reply.AccountID,
|
||||
InteractingAccount: reply.Account,
|
||||
InteractionRequestURI: reply.URI + gtsmodel.ImpoliteReplyRequestSuffix,
|
||||
InteractionURI: reply.URI,
|
||||
InteractionType: gtsmodel.InteractionReply,
|
||||
Polite: util.Ptr(false),
|
||||
Reply: reply,
|
||||
}
|
||||
if err := suite.state.DB.PutInteractionRequest(ctx, intReq); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
@@ -83,9 +96,23 @@ func (suite *InteractionTestSuite) markInteractionsPending(
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Put an impolite interaction request in the DB for this boost.
|
||||
req := typeutils.StatusToImpoliteInteractionRequest(boost)
|
||||
if err := suite.state.DB.PutInteractionRequest(ctx, req); err != nil {
|
||||
// Put an interaction request in the DB for this boost.
|
||||
intReqID := id.NewULIDFromTime(boost.CreatedAt)
|
||||
intReq := >smodel.InteractionRequest{
|
||||
ID: intReqID,
|
||||
TargetStatusID: boost.BoostOfID,
|
||||
TargetStatus: boost.BoostOf,
|
||||
TargetAccountID: boost.BoostOfAccountID,
|
||||
TargetAccount: boost.BoostOfAccount,
|
||||
InteractingAccountID: boost.AccountID,
|
||||
InteractingAccount: boost.Account,
|
||||
InteractionRequestURI: boost.URI + gtsmodel.ImpoliteAnnounceRequestSuffix,
|
||||
InteractionURI: boost.URI,
|
||||
InteractionType: gtsmodel.InteractionAnnounce,
|
||||
Polite: util.Ptr(false),
|
||||
Announce: boost,
|
||||
}
|
||||
if err := suite.state.DB.PutInteractionRequest(ctx, intReq); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
@@ -110,8 +137,22 @@ func (suite *InteractionTestSuite) markInteractionsPending(
|
||||
}
|
||||
|
||||
// Put an impolite interaction request in the DB for this fave.
|
||||
req := typeutils.StatusFaveToImpoliteInteractionRequest(fave)
|
||||
if err := suite.state.DB.PutInteractionRequest(ctx, req); err != nil {
|
||||
intReqID := id.NewULIDFromTime(fave.CreatedAt)
|
||||
intReq := >smodel.InteractionRequest{
|
||||
ID: intReqID,
|
||||
TargetStatusID: fave.StatusID,
|
||||
TargetStatus: fave.Status,
|
||||
TargetAccountID: fave.TargetAccountID,
|
||||
TargetAccount: fave.TargetAccount,
|
||||
InteractingAccountID: fave.AccountID,
|
||||
InteractingAccount: fave.Account,
|
||||
InteractionRequestURI: fave.URI + gtsmodel.ImpoliteLikeRequestSuffix,
|
||||
InteractionURI: fave.URI,
|
||||
InteractionType: gtsmodel.InteractionLike,
|
||||
Polite: util.Ptr(false),
|
||||
Like: fave,
|
||||
}
|
||||
if err := suite.state.DB.PutInteractionRequest(ctx, intReq); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
|
||||
@@ -372,7 +372,7 @@ func (d *Dereferencer) unpermittedByParent(
|
||||
TargetStatusID: inReplyToID,
|
||||
TargetAccountID: targetAccountID,
|
||||
InteractingAccountID: reply.AccountID,
|
||||
InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(reply.URI, gtsmodel.ReplyRequestSuffix),
|
||||
InteractionRequestURI: reply.URI + gtsmodel.ImpoliteReplyRequestSuffix,
|
||||
InteractionURI: reply.URI,
|
||||
InteractionType: gtsmodel.InteractionReply,
|
||||
Polite: util.Ptr(false),
|
||||
@@ -506,7 +506,7 @@ func (d *Dereferencer) rejectedByPolicy(
|
||||
TargetStatusID: inReplyTo.ID,
|
||||
TargetAccountID: inReplyTo.AccountID,
|
||||
InteractingAccountID: reply.AccountID,
|
||||
InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(reply.URI, gtsmodel.ReplyRequestSuffix),
|
||||
InteractionRequestURI: reply.URI + gtsmodel.ImpoliteReplyRequestSuffix,
|
||||
InteractionURI: reply.URI,
|
||||
InteractionType: gtsmodel.InteractionReply,
|
||||
Polite: util.Ptr(false),
|
||||
|
||||
@@ -313,14 +313,14 @@ func (f *DB) rejectStatusIRI(
|
||||
|
||||
if apObjectType == ap.ObjectNote {
|
||||
// Reply.
|
||||
req.InteractionRequestURI = gtsmodel.ForwardCompatibleInteractionRequestURI(status.URI, gtsmodel.ReplyRequestSuffix)
|
||||
req.InteractionRequestURI = status.URI + gtsmodel.ImpoliteReplyRequestSuffix
|
||||
req.InteractionType = gtsmodel.InteractionReply
|
||||
req.TargetStatusID = status.InReplyToID
|
||||
req.TargetStatus = status.InReplyTo
|
||||
req.Reply = status
|
||||
} else {
|
||||
// Announce.
|
||||
req.InteractionRequestURI = gtsmodel.ForwardCompatibleInteractionRequestURI(status.URI, gtsmodel.AnnounceRequestSuffix)
|
||||
req.InteractionRequestURI = status.URI + gtsmodel.ImpoliteAnnounceRequestSuffix
|
||||
req.InteractionType = gtsmodel.InteractionAnnounce
|
||||
req.TargetStatusID = status.BoostOfID
|
||||
req.TargetStatus = status.BoostOf
|
||||
@@ -439,7 +439,7 @@ func (f *DB) rejectLikeIRI(
|
||||
TargetAccount: requestingAcct,
|
||||
InteractingAccountID: receivingAcct.ID,
|
||||
InteractingAccount: receivingAcct,
|
||||
InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(fave.URI, gtsmodel.LikeRequestSuffix),
|
||||
InteractionRequestURI: fave.URI + gtsmodel.ImpoliteLikeRequestSuffix,
|
||||
InteractionURI: fave.URI,
|
||||
InteractionType: gtsmodel.InteractionLike,
|
||||
Polite: util.Ptr(false),
|
||||
|
||||
@@ -17,7 +17,11 @@
|
||||
|
||||
package gtsmodel
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.superseriousbusiness.org/gotosocial/internal/uris"
|
||||
)
|
||||
|
||||
// Like / Reply / Announce
|
||||
type InteractionType enumType
|
||||
@@ -37,26 +41,17 @@ const (
|
||||
const (
|
||||
// Suffix to append to the URI of
|
||||
// impolite Likes to mock a LikeRequest.
|
||||
LikeRequestSuffix = "#LikeRequest"
|
||||
ImpoliteLikeRequestSuffix = "#LikeRequest"
|
||||
|
||||
// Suffix to append to the URI of
|
||||
// impolite replies to mock a ReplyRequest.
|
||||
ReplyRequestSuffix = "#ReplyRequest"
|
||||
ImpoliteReplyRequestSuffix = "#ReplyRequest"
|
||||
|
||||
// Suffix to append to the URI of impolite
|
||||
// Announces to mock an AnnounceRequest.
|
||||
AnnounceRequestSuffix = "#AnnounceRequest"
|
||||
ImpoliteAnnounceRequestSuffix = "#AnnounceRequest"
|
||||
)
|
||||
|
||||
// A useless function that appends two strings, this exists largely
|
||||
// to indicate where a request URI is being generated as forward compatible
|
||||
// with our planned polite request flow fully introduced in v0.21.0.
|
||||
//
|
||||
// TODO: remove this in v0.21.0. everything the linter complains about after removing this, needs updating.
|
||||
func ForwardCompatibleInteractionRequestURI(interactionURI string, suffix string) string {
|
||||
return interactionURI + suffix
|
||||
}
|
||||
|
||||
// Stringifies this InteractionType in a
|
||||
// manner suitable for serving via the API.
|
||||
func (i InteractionType) String() string {
|
||||
@@ -86,7 +81,7 @@ type InteractionRequest struct {
|
||||
// ID of the status targeted by the interaction.
|
||||
TargetStatusID string `bun:"type:CHAR(26),nullzero,notnull"`
|
||||
|
||||
// Local status corresponding to TargetStatusID.
|
||||
// Status corresponding to TargetStatusID.
|
||||
// Column not stored in DB.
|
||||
TargetStatus *Status `bun:"-"`
|
||||
|
||||
@@ -163,12 +158,39 @@ func (ir *InteractionRequest) IsAccepted() bool {
|
||||
return !ir.AcceptedAt.IsZero()
|
||||
}
|
||||
|
||||
// MarkAccepted marks the interaction request as
|
||||
// accepted by the target account, by updating the
|
||||
// AcceptedAt, ResponseURI, and AuthorizationURI fields.
|
||||
//
|
||||
// TargetAccount must be set or this will panic!
|
||||
func (ir *InteractionRequest) MarkAccepted() {
|
||||
ir.AcceptedAt = time.Now()
|
||||
ir.ResponseURI = uris.GenerateURIForAccept(
|
||||
ir.TargetAccount.Username, ir.ID,
|
||||
)
|
||||
ir.AuthorizationURI = uris.GenerateURIForAuthorization(
|
||||
ir.TargetAccount.Username, ir.ID,
|
||||
)
|
||||
}
|
||||
|
||||
// IsRejected returns true if this
|
||||
// interaction request has been rejected.
|
||||
func (ir *InteractionRequest) IsRejected() bool {
|
||||
return !ir.RejectedAt.IsZero()
|
||||
}
|
||||
|
||||
// MarkRejected marks the interaction request
|
||||
// as rejected by the target account, by updating
|
||||
// the RejectedAt and ResponseURI fields.
|
||||
//
|
||||
// TargetAccount must be set or this will panic!
|
||||
func (ir *InteractionRequest) MarkRejected() {
|
||||
ir.RejectedAt = time.Now()
|
||||
ir.ResponseURI = uris.GenerateURIForReject(
|
||||
ir.TargetAccount.Username, ir.ID,
|
||||
)
|
||||
}
|
||||
|
||||
// IsPolite returns true if this interaction request was done
|
||||
// "politely" with a *Request type, or false if it was done
|
||||
// "impolitely" with direct send of a like, reply, or announce.
|
||||
|
||||
@@ -417,8 +417,3 @@ type Content struct {
|
||||
Content string
|
||||
ContentMap map[string]string
|
||||
}
|
||||
|
||||
// BackfillStatus is a wrapper for creating a status without pushing notifications to followers.
|
||||
type BackfillStatus struct {
|
||||
*Status
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
// 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 fedi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"code.superseriousbusiness.org/gotosocial/internal/ap"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/db"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
func (p *Processor) LikeRequestGet(
|
||||
ctx context.Context,
|
||||
requestedUser string,
|
||||
id string,
|
||||
) (any, gtserror.WithCode) {
|
||||
intReq, errWithCode := p.interactionRequestGet(ctx, requestedUser, id)
|
||||
if errWithCode != nil {
|
||||
return nil, errWithCode
|
||||
}
|
||||
|
||||
if intReq.InteractionType != gtsmodel.InteractionLike {
|
||||
const text = "interaction request was not LikeRequest"
|
||||
return nil, gtserror.NewErrorNotFound(errors.New(text))
|
||||
}
|
||||
|
||||
return p.intReqData(ctx, intReq)
|
||||
}
|
||||
|
||||
func (p *Processor) ReplyRequestGet(
|
||||
ctx context.Context,
|
||||
requestedUser string,
|
||||
id string,
|
||||
) (any, gtserror.WithCode) {
|
||||
intReq, errWithCode := p.interactionRequestGet(ctx, requestedUser, id)
|
||||
if errWithCode != nil {
|
||||
return nil, errWithCode
|
||||
}
|
||||
|
||||
if intReq.InteractionType != gtsmodel.InteractionReply {
|
||||
const text = "interaction request was not ReplyRequest"
|
||||
return nil, gtserror.NewErrorNotFound(errors.New(text))
|
||||
}
|
||||
|
||||
return p.intReqData(ctx, intReq)
|
||||
}
|
||||
|
||||
func (p *Processor) AnnounceRequestGet(
|
||||
ctx context.Context,
|
||||
requestedUser string,
|
||||
id string,
|
||||
) (any, gtserror.WithCode) {
|
||||
intReq, errWithCode := p.interactionRequestGet(ctx, requestedUser, id)
|
||||
if errWithCode != nil {
|
||||
return nil, errWithCode
|
||||
}
|
||||
|
||||
if intReq.InteractionType != gtsmodel.InteractionAnnounce {
|
||||
const text = "interaction request was not AnnounceRequest"
|
||||
return nil, gtserror.NewErrorNotFound(errors.New(text))
|
||||
}
|
||||
|
||||
return p.intReqData(ctx, intReq)
|
||||
}
|
||||
|
||||
func (p *Processor) interactionRequestGet(
|
||||
ctx context.Context,
|
||||
requestedUser string,
|
||||
id string,
|
||||
) (*gtsmodel.InteractionRequest, gtserror.WithCode) {
|
||||
// Authenticate incoming request, getting related accounts.
|
||||
auth, errWithCode := p.authenticate(ctx, requestedUser)
|
||||
if errWithCode != nil {
|
||||
return nil, errWithCode
|
||||
}
|
||||
|
||||
if auth.handshakingURI != nil {
|
||||
// We're currently handshaking, which means
|
||||
// we don't know this account yet. This should
|
||||
// be a very rare race condition.
|
||||
err := gtserror.Newf("network race handshaking %s", auth.handshakingURI)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
receiver := auth.receiver
|
||||
requester := auth.requester
|
||||
|
||||
intReq, err := p.state.DB.GetInteractionRequestByID(ctx, id)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
err := gtserror.Newf("db error getting interaction request: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if intReq == nil {
|
||||
err := gtserror.Newf("interaction request %s not found in the db", id)
|
||||
return nil, gtserror.NewErrorNotFound(err)
|
||||
}
|
||||
|
||||
// Interaction request must be owned
|
||||
// by receiving account / requestedUser.
|
||||
if intReq.InteractingAccountID != receiver.ID {
|
||||
const text = "interaction request does not belong to receiving account"
|
||||
return nil, gtserror.NewErrorNotFound(errors.New(text))
|
||||
}
|
||||
|
||||
// Requester must be either the owner of
|
||||
// the interaction request or the target.
|
||||
if requester.ID != intReq.TargetAccountID &&
|
||||
requester.ID != intReq.InteractingAccountID {
|
||||
const text = "interaction request not visible to requesting account"
|
||||
return nil, gtserror.NewErrorNotFound(errors.New(text))
|
||||
}
|
||||
|
||||
// Only polite interaction requests can
|
||||
// be converted to InteractionRequestable.
|
||||
if !intReq.IsPolite() {
|
||||
const text = "interaction request not polite"
|
||||
return nil, gtserror.NewErrorNotFound(errors.New(text))
|
||||
}
|
||||
|
||||
return intReq, nil
|
||||
}
|
||||
|
||||
func (p *Processor) intReqData(ctx context.Context, intReq *gtsmodel.InteractionRequest) (any, gtserror.WithCode) {
|
||||
intRequestable, err := p.converter.InteractionReqToASInteractionRequestable(ctx, intReq)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error converting interaction request: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
data, err := ap.Serialize(intRequestable)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error serializing interaction request: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
@@ -147,15 +147,29 @@ func (p *Processor) BoostCreate(
|
||||
target.PendingApproval = util.Ptr(false)
|
||||
}
|
||||
|
||||
// Queue remaining boost side effects
|
||||
// (send out boost, update timeline, etc).
|
||||
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
|
||||
APObjectType: ap.ActivityAnnounce,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: boost,
|
||||
Origin: requester,
|
||||
Target: target.Account,
|
||||
})
|
||||
if pendingApproval {
|
||||
// Boost is pending approval, which means it
|
||||
// must target a status with an interaction
|
||||
// policy that requires approval for announces.
|
||||
// Queue up Create AnnounceRequest side effects.
|
||||
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
|
||||
APObjectType: ap.ActivityAnnounceRequest,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: boost,
|
||||
Origin: requester,
|
||||
Target: target.Account,
|
||||
})
|
||||
} else {
|
||||
// "Normal" boost with no explicit approval
|
||||
// required, queue Create Announce side effects.
|
||||
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
|
||||
APObjectType: ap.ActivityAnnounce,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: boost,
|
||||
Origin: requester,
|
||||
Target: target.Account,
|
||||
})
|
||||
}
|
||||
|
||||
return p.c.GetAPIStatus(ctx, requester, boost)
|
||||
}
|
||||
|
||||
@@ -302,21 +302,35 @@ func (p *Processor) Create(
|
||||
status.InReplyTo.PendingApproval = util.Ptr(false)
|
||||
}
|
||||
|
||||
var model any = status
|
||||
if backfill {
|
||||
// We specifically wrap backfilled statuses in
|
||||
// a different type to signal to worker process.
|
||||
model = >smodel.BackfillStatus{Status: status}
|
||||
}
|
||||
switch {
|
||||
case backfill:
|
||||
// Don't queue side effects of status creation
|
||||
// if this is a backfill status. For backfill
|
||||
// statuses, just inserting them in the database
|
||||
// is enough. We shouldn't federate, notify, etc.
|
||||
|
||||
// Queue remaining create side effects
|
||||
// (send out status, update timeline, etc).
|
||||
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
|
||||
APObjectType: ap.ObjectNote,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: model,
|
||||
Origin: requester,
|
||||
})
|
||||
case *status.PendingApproval:
|
||||
// Status is pending approval, which means it
|
||||
// must be a reply to a status with an interaction
|
||||
// policy that requires approval for replies.
|
||||
// Queue up Create ReplyRequest side effects.
|
||||
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
|
||||
APObjectType: ap.ActivityReplyRequest,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: status,
|
||||
Origin: requester,
|
||||
})
|
||||
|
||||
default:
|
||||
// "Normal" status with no explicit approval
|
||||
// required, queue Create Status side effects.
|
||||
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
|
||||
APObjectType: ap.ObjectNote,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: status,
|
||||
Origin: requester,
|
||||
})
|
||||
}
|
||||
|
||||
return p.c.GetAPIStatus(ctx, requester, status)
|
||||
}
|
||||
|
||||
@@ -177,15 +177,29 @@ func (p *Processor) FaveCreate(
|
||||
status.PendingApproval = util.Ptr(false)
|
||||
}
|
||||
|
||||
// Queue remaining fave side effects
|
||||
// (send out fave, update timeline, etc).
|
||||
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
|
||||
APObjectType: ap.ActivityLike,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: gtsFave,
|
||||
Origin: requester,
|
||||
Target: status.Account,
|
||||
})
|
||||
if pendingApproval {
|
||||
// Fave is pending approval, which means it
|
||||
// must target a status with an interaction
|
||||
// policy that requires approval for faves.
|
||||
// Queue up Create LikeRequest side effects.
|
||||
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
|
||||
APObjectType: ap.ActivityLikeRequest,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: gtsFave,
|
||||
Origin: requester,
|
||||
Target: status.Account,
|
||||
})
|
||||
} else {
|
||||
// "Normal" fave with no explicit approval
|
||||
// required, queue Create Like side effects.
|
||||
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
|
||||
APObjectType: ap.ActivityLike,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: gtsFave,
|
||||
Origin: requester,
|
||||
Target: status.Account,
|
||||
})
|
||||
}
|
||||
|
||||
return p.c.GetAPIStatus(ctx, requester, status)
|
||||
}
|
||||
|
||||
@@ -22,14 +22,12 @@ import (
|
||||
"net/url"
|
||||
|
||||
"code.superseriousbusiness.org/activity/streams"
|
||||
"code.superseriousbusiness.org/activity/streams/vocab"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/ap"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/federation"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/state"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/typeutils"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// federate wraps functions for federating
|
||||
@@ -133,10 +131,6 @@ func (f *federate) DeleteAccount(ctx context.Context, account *gtsmodel.Account)
|
||||
|
||||
// CreateStatus sends the given status out to relevant
|
||||
// recipients with the Outbox of the status creator.
|
||||
//
|
||||
// If the status is pending approval, then it will be
|
||||
// sent **ONLY** to the inbox of the account it replies to,
|
||||
// ignoring shared inboxes.
|
||||
func (f *federate) CreateStatus(ctx context.Context, status *gtsmodel.Status) error {
|
||||
// Do nothing if the status
|
||||
// shouldn't be federated.
|
||||
@@ -161,20 +155,6 @@ func (f *federate) CreateStatus(ctx context.Context, status *gtsmodel.Status) er
|
||||
return gtserror.Newf("error converting status to Statusable: %w", err)
|
||||
}
|
||||
|
||||
// If status is pending approval,
|
||||
// it must be a reply. Deliver it
|
||||
// **ONLY** to the account it replies
|
||||
// to, on behalf of the replier.
|
||||
if util.PtrOrValue(status.PendingApproval, false) {
|
||||
return f.deliverToInboxOnly(
|
||||
ctx,
|
||||
status.Account,
|
||||
status.InReplyToAccount,
|
||||
// Status has to be wrapped in Create activity.
|
||||
typeutils.WrapStatusableInCreate(statusable, false),
|
||||
)
|
||||
}
|
||||
|
||||
// Parse the outbox URI of the status author.
|
||||
outboxIRI, err := parseURI(status.Account.OutboxURI)
|
||||
if err != nil {
|
||||
@@ -690,10 +670,6 @@ func (f *federate) RejectFollow(ctx context.Context, follow *gtsmodel.Follow) er
|
||||
|
||||
// Like sends the given fave out to relevant
|
||||
// recipients with the Outbox of the status creator.
|
||||
//
|
||||
// If the fave is pending approval, then it will be
|
||||
// sent **ONLY** to the inbox of the account it faves,
|
||||
// ignoring shared inboxes.
|
||||
func (f *federate) Like(ctx context.Context, fave *gtsmodel.StatusFave) error {
|
||||
// Populate model.
|
||||
if err := f.state.DB.PopulateStatusFave(ctx, fave); err != nil {
|
||||
@@ -712,18 +688,6 @@ func (f *federate) Like(ctx context.Context, fave *gtsmodel.StatusFave) error {
|
||||
return gtserror.Newf("error converting fave to AS Like: %w", err)
|
||||
}
|
||||
|
||||
// If fave is pending approval,
|
||||
// deliver it **ONLY** to the account
|
||||
// it faves, on behalf of the faver.
|
||||
if util.PtrOrValue(fave.PendingApproval, false) {
|
||||
return f.deliverToInboxOnly(
|
||||
ctx,
|
||||
fave.Account,
|
||||
fave.TargetAccount,
|
||||
like,
|
||||
)
|
||||
}
|
||||
|
||||
// Parse relevant URI(s).
|
||||
outboxIRI, err := parseURI(fave.Account.OutboxURI)
|
||||
if err != nil {
|
||||
@@ -745,10 +709,6 @@ func (f *federate) Like(ctx context.Context, fave *gtsmodel.StatusFave) error {
|
||||
|
||||
// Announce sends the given boost out to relevant
|
||||
// recipients with the Outbox of the status creator.
|
||||
//
|
||||
// If the boost is pending approval, then it will be
|
||||
// sent **ONLY** to the inbox of the account it boosts,
|
||||
// ignoring shared inboxes.
|
||||
func (f *federate) Announce(ctx context.Context, boost *gtsmodel.Status) error {
|
||||
// Populate model.
|
||||
if err := f.state.DB.PopulateStatus(ctx, boost); err != nil {
|
||||
@@ -767,18 +727,6 @@ func (f *federate) Announce(ctx context.Context, boost *gtsmodel.Status) error {
|
||||
return gtserror.Newf("error converting boost to AS: %w", err)
|
||||
}
|
||||
|
||||
// If announce is pending approval,
|
||||
// deliver it **ONLY** to the account
|
||||
// it boosts, on behalf of the booster.
|
||||
if util.PtrOrValue(boost.PendingApproval, false) {
|
||||
return f.deliverToInboxOnly(
|
||||
ctx,
|
||||
boost.Account,
|
||||
boost.BoostOfAccount,
|
||||
announce,
|
||||
)
|
||||
}
|
||||
|
||||
// Parse relevant URI(s).
|
||||
outboxIRI, err := parseURI(boost.Account.OutboxURI)
|
||||
if err != nil {
|
||||
@@ -798,57 +746,6 @@ func (f *federate) Announce(ctx context.Context, boost *gtsmodel.Status) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// deliverToInboxOnly delivers the given Activity
|
||||
// *only* to the inbox of targetAcct, on behalf of
|
||||
// sendingAcct, regardless of the `to` and `cc` values
|
||||
// set on the activity. This should be used specifically
|
||||
// for sending "pending approval" activities.
|
||||
func (f *federate) deliverToInboxOnly(
|
||||
ctx context.Context,
|
||||
sendingAcct *gtsmodel.Account,
|
||||
targetAcct *gtsmodel.Account,
|
||||
t vocab.Type,
|
||||
) error {
|
||||
if targetAcct.IsLocal() {
|
||||
// If this is a local target,
|
||||
// they've already received it.
|
||||
return nil
|
||||
}
|
||||
|
||||
toInbox, err := url.Parse(targetAcct.InboxURI)
|
||||
if err != nil {
|
||||
return gtserror.Newf(
|
||||
"error parsing target inbox uri: %w",
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
tsport, err := f.TransportController().NewTransportForUsername(
|
||||
ctx,
|
||||
sendingAcct.Username,
|
||||
)
|
||||
if err != nil {
|
||||
return gtserror.Newf(
|
||||
"error getting transport to deliver activity %T to target inbox %s: %w",
|
||||
t, targetAcct.InboxURI, err,
|
||||
)
|
||||
}
|
||||
|
||||
m, err := ap.Serialize(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tsport.Deliver(ctx, m, toInbox); err != nil {
|
||||
return gtserror.Newf(
|
||||
"error delivering activity %T to target inbox %s: %w",
|
||||
t, targetAcct.InboxURI, err,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *federate) UpdateAccount(ctx context.Context, account *gtsmodel.Account) error {
|
||||
// Populate model.
|
||||
if err := f.state.DB.PopulateAccount(ctx, account); err != nil {
|
||||
@@ -1206,3 +1103,56 @@ func (f *federate) RejectInteraction(
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InteractionRequest sends out the given
|
||||
// *gtsmodel.InteractionRequest as a polite
|
||||
// LikeRequest, ReplyRequest, or AnnounceRequest,
|
||||
// to the interaction target's inbox.
|
||||
func (f *federate) InteractionRequest(
|
||||
ctx context.Context,
|
||||
req *gtsmodel.InteractionRequest,
|
||||
) error {
|
||||
// Populate model.
|
||||
if err := f.state.DB.PopulateInteractionRequest(ctx, req); err != nil {
|
||||
return gtserror.Newf("error populating request: %w", err)
|
||||
}
|
||||
|
||||
// Bail if the interacter is remote
|
||||
// or the target is local, we don't
|
||||
// need to do anything then.
|
||||
if req.InteractingAccount.IsRemote() ||
|
||||
req.TargetAccount.IsLocal() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Bail if the request is
|
||||
// already approved or rejected.
|
||||
if !req.IsPending() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse outbox URI.
|
||||
outboxIRI, err := parseURI(req.InteractingAccount.OutboxURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert req to InteractionRequestable.
|
||||
intReqable, err := f.converter.InteractionReqToASInteractionRequestable(ctx, req)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error converting request to InteractionRequestable: %w", err)
|
||||
}
|
||||
|
||||
// Send the interaction request
|
||||
// via the Actor's outbox.
|
||||
if _, err := f.FederatingActor().Send(
|
||||
ctx, outboxIRI, intReqable,
|
||||
); err != nil {
|
||||
return gtserror.Newf(
|
||||
"error sending activity %T for %v via outbox %s: %w",
|
||||
intReqable, req.InteractionType, outboxIRI, err,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ package workers
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"code.superseriousbusiness.org/gopkg/log"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/ap"
|
||||
@@ -91,6 +90,10 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro
|
||||
case ap.ActivityQuestion:
|
||||
return p.clientAPI.CreatePollVote(ctx, cMsg)
|
||||
|
||||
// CREATE REPLY REQUEST
|
||||
case ap.ActivityReplyRequest:
|
||||
return p.clientAPI.CreateReplyRequest(ctx, cMsg)
|
||||
|
||||
// CREATE FOLLOW (request)
|
||||
case ap.ActivityFollow:
|
||||
return p.clientAPI.CreateFollowReq(ctx, cMsg)
|
||||
@@ -99,10 +102,18 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro
|
||||
case ap.ActivityLike:
|
||||
return p.clientAPI.CreateLike(ctx, cMsg)
|
||||
|
||||
// CREATE LIKE REQUEST
|
||||
case ap.ActivityLikeRequest:
|
||||
return p.clientAPI.CreateLikeRequest(ctx, cMsg)
|
||||
|
||||
// CREATE ANNOUNCE/BOOST
|
||||
case ap.ActivityAnnounce:
|
||||
return p.clientAPI.CreateAnnounce(ctx, cMsg)
|
||||
|
||||
// CREATE ANNOUNCE REQUEST
|
||||
case ap.ActivityAnnounceRequest:
|
||||
return p.clientAPI.CreateAnnounceRequest(ctx, cMsg)
|
||||
|
||||
// CREATE BLOCK
|
||||
case ap.ActivityBlock:
|
||||
return p.clientAPI.CreateBlock(ctx, cMsg)
|
||||
@@ -260,110 +271,115 @@ func (p *clientAPI) CreateUser(ctx context.Context, cMsg *messages.FromClientAPI
|
||||
}
|
||||
|
||||
func (p *clientAPI) CreateStatus(ctx context.Context, cMsg *messages.FromClientAPI) error {
|
||||
var status *gtsmodel.Status
|
||||
var backfill bool
|
||||
|
||||
// Check passed client message model type.
|
||||
switch model := cMsg.GTSModel.(type) {
|
||||
case *gtsmodel.Status:
|
||||
status = model
|
||||
case *gtsmodel.BackfillStatus:
|
||||
status = model.Status
|
||||
backfill = true
|
||||
default:
|
||||
return gtserror.Newf("%T not parseable as *gtsmodel.Status or *gtsmodel.BackfillStatus", cMsg.GTSModel)
|
||||
status, ok := cMsg.GTSModel.(*gtsmodel.Status)
|
||||
if !ok {
|
||||
return gtserror.Newf("%T not parseable as *gtsmodel.Status", cMsg.GTSModel)
|
||||
}
|
||||
|
||||
// If pending approval is true then status must
|
||||
// reply to a status (either one of ours or a
|
||||
// remote) that requires approval for the reply.
|
||||
pendingApproval := util.PtrOrZero(status.PendingApproval)
|
||||
if err := p.surface.timelineAndNotifyStatus(ctx, status); err != nil {
|
||||
log.Errorf(ctx, "error timelining and notifying status: %v", err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case pendingApproval && !status.PreApproved:
|
||||
// If approval is required and status isn't
|
||||
// preapproved, then send out the Create to
|
||||
// only the replied-to account (if it's remote),
|
||||
// and/or notify the account that's being
|
||||
// interacted with (if it's local): they can
|
||||
// approve or deny the interaction later.
|
||||
if err := p.utils.impoliteReplyRequest(ctx, status); err != nil {
|
||||
return gtserror.Newf("error pending reply: %w", err)
|
||||
}
|
||||
if err := p.federate.CreateStatus(ctx, status); err != nil {
|
||||
log.Errorf(ctx, "error federating status: %v", err)
|
||||
}
|
||||
|
||||
// Send Create to *remote* account inbox ONLY.
|
||||
if err := p.federate.CreateStatus(ctx, status); err != nil {
|
||||
return gtserror.Newf("error federating pending reply: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return early.
|
||||
return nil
|
||||
func (p *clientAPI) CreateReplyRequest(ctx context.Context, cMsg *messages.FromClientAPI) error {
|
||||
reply, ok := cMsg.GTSModel.(*gtsmodel.Status)
|
||||
if !ok {
|
||||
return gtserror.Newf("%T not parseable as *gtsmodel.Status", cMsg.GTSModel)
|
||||
}
|
||||
|
||||
case pendingApproval && status.PreApproved:
|
||||
// If approval is required and status is
|
||||
// preapproved, that means this is a reply
|
||||
// to one of our statuses with permission
|
||||
// that matched on a following/followers
|
||||
// collection. Do the Accept immediately and
|
||||
// then process everything else as normal,
|
||||
// sending out the Create with the approval
|
||||
// URI attached.
|
||||
// Create a polite reply request.
|
||||
intReqID := id.NewULIDFromTime(reply.CreatedAt)
|
||||
intReq := >smodel.InteractionRequest{
|
||||
ID: intReqID,
|
||||
TargetStatusID: reply.InReplyToID,
|
||||
TargetStatus: reply.InReplyTo,
|
||||
TargetAccountID: reply.InReplyToAccountID,
|
||||
TargetAccount: reply.InReplyToAccount,
|
||||
InteractingAccountID: reply.AccountID,
|
||||
InteractingAccount: reply.Account,
|
||||
InteractionRequestURI: uris.GenerateURIForReplyRequest(reply.Account.Username, intReqID),
|
||||
InteractionURI: reply.URI,
|
||||
InteractionType: gtsmodel.InteractionReply,
|
||||
Polite: util.Ptr(true),
|
||||
Reply: reply,
|
||||
}
|
||||
|
||||
// Store an already-accepted interaction request.
|
||||
requestID := id.NewULID()
|
||||
approval := >smodel.InteractionRequest{
|
||||
ID: requestID,
|
||||
TargetStatusID: status.InReplyToID,
|
||||
TargetAccountID: status.InReplyToAccountID,
|
||||
TargetAccount: status.InReplyToAccount,
|
||||
InteractingAccountID: status.AccountID,
|
||||
InteractingAccount: status.Account,
|
||||
InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(status.URI, gtsmodel.ReplyRequestSuffix),
|
||||
InteractionURI: status.URI,
|
||||
InteractionType: gtsmodel.InteractionReply,
|
||||
Polite: util.Ptr(false), // TODO: Change this in v0.21.0 when we only send out polite requests.
|
||||
Reply: status,
|
||||
ResponseURI: uris.GenerateURIForAccept(status.InReplyToAccount.Username, requestID),
|
||||
AuthorizationURI: uris.GenerateURIForAuthorization(status.InReplyToAccount.Username, requestID),
|
||||
AcceptedAt: time.Now(),
|
||||
}
|
||||
if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil {
|
||||
return gtserror.Newf("db error putting pre-approved interaction request: %w", err)
|
||||
}
|
||||
|
||||
// Mark the status as now approved.
|
||||
status.PendingApproval = util.Ptr(false)
|
||||
status.PreApproved = false
|
||||
status.ApprovedByURI = approval.AuthorizationURI
|
||||
if err := p.state.DB.UpdateStatus(
|
||||
ctx,
|
||||
status,
|
||||
"pending_approval",
|
||||
"approved_by_uri",
|
||||
if !reply.PreApproved {
|
||||
// If the reply is not pre-approved, just
|
||||
// store the interaction request, notify
|
||||
// (local) target or federate request.
|
||||
if err := p.utils.storeInteractionRequest(
|
||||
ctx, intReq,
|
||||
); err != nil {
|
||||
return gtserror.Newf("db error updating status: %w", err)
|
||||
return gtserror.Newf("error storing interaction request: %w", err)
|
||||
}
|
||||
|
||||
if err := p.federate.AcceptInteraction(ctx, approval); err != nil {
|
||||
return gtserror.Newf("error federating pre-approval of reply: %w", err)
|
||||
// Notify target account (if local) of pending reply.
|
||||
if err := p.surface.notifyPendingReply(ctx, intReq.Reply); err != nil {
|
||||
return gtserror.Newf("error notifying pending reply: %w", err)
|
||||
}
|
||||
|
||||
// Don't return, just continue as normal.
|
||||
// Send interaction request to target account (if remote).
|
||||
if err := p.federate.InteractionRequest(ctx, intReq); err != nil {
|
||||
return gtserror.Newf("error federating interaction request: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// We specifically do not timeline
|
||||
// or notify for backfilled statuses,
|
||||
// as these are more for archival than
|
||||
// newly posted content for user feeds.
|
||||
if !backfill {
|
||||
// If the reply is pre-approved, then it must
|
||||
// target a status on our instance, and the
|
||||
// replier gets automatic approval due to being
|
||||
// in the author's followers/following collection.
|
||||
//
|
||||
// Mark the interaction request as accepted, store it,
|
||||
// mark the interaction as approved, and then continue
|
||||
// with side effects as normal.
|
||||
|
||||
if err := p.surface.timelineAndNotifyStatus(ctx, status); err != nil {
|
||||
log.Errorf(ctx, "error timelining and notifying status: %v", err)
|
||||
}
|
||||
// Update intReq fields to
|
||||
// mark it as accepted.
|
||||
intReq.MarkAccepted()
|
||||
|
||||
if err := p.federate.CreateStatus(ctx, status); err != nil {
|
||||
log.Errorf(ctx, "error federating status: %v", err)
|
||||
}
|
||||
// Put it in the DB.
|
||||
if err := p.utils.storeInteractionRequest(
|
||||
ctx, intReq,
|
||||
); err != nil {
|
||||
return gtserror.Newf("error storing interaction request: %w", err)
|
||||
}
|
||||
|
||||
// Mark the status as now approved, referring to
|
||||
// the accepted interaction request we just stored.
|
||||
reply.PendingApproval = util.Ptr(false)
|
||||
reply.PreApproved = false
|
||||
reply.ApprovedByURI = intReq.AuthorizationURI
|
||||
if err := p.state.DB.UpdateStatus(
|
||||
ctx,
|
||||
reply,
|
||||
"pending_approval",
|
||||
"approved_by_uri",
|
||||
); err != nil {
|
||||
return gtserror.Newf("db error updating status: %w", err)
|
||||
}
|
||||
|
||||
// Send out the approval as Accept.
|
||||
if err := p.federate.AcceptInteraction(ctx, intReq); err != nil {
|
||||
return gtserror.Newf("error federating pre-approval of reply: %w", err)
|
||||
}
|
||||
|
||||
// Timeline + notify the reply.
|
||||
if err := p.surface.timelineAndNotifyStatus(ctx, reply); err != nil {
|
||||
log.Errorf(ctx, "error timelining and notifying status: %v", err)
|
||||
}
|
||||
|
||||
// Send out the approved reply.
|
||||
if err := p.federate.CreateStatus(ctx, reply); err != nil {
|
||||
log.Errorf(ctx, "error federating status: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -465,83 +481,6 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg *messages.FromClientAPI
|
||||
return gtserror.Newf("error populating status fave: %w", err)
|
||||
}
|
||||
|
||||
// If pending approval is true then fave must
|
||||
// target a status (either one of ours or a
|
||||
// remote) that requires approval for the fave.
|
||||
pendingApproval := util.PtrOrZero(fave.PendingApproval)
|
||||
|
||||
switch {
|
||||
case pendingApproval && !fave.PreApproved:
|
||||
// If approval is required and fave isn't
|
||||
// preapproved, then send out the Like to
|
||||
// only the faved account (if it's remote),
|
||||
// and/or notify the account that's being
|
||||
// interacted with (if it's local): they can
|
||||
// approve or deny the interaction later.
|
||||
if err := p.utils.impoliteFaveRequest(ctx, fave); err != nil {
|
||||
return gtserror.Newf("error pending fave: %w", err)
|
||||
}
|
||||
|
||||
// Send Like to *remote* account inbox ONLY.
|
||||
if err := p.federate.Like(ctx, fave); err != nil {
|
||||
return gtserror.Newf("error federating pending Like: %v", err)
|
||||
}
|
||||
|
||||
// Return early.
|
||||
return nil
|
||||
|
||||
case pendingApproval && fave.PreApproved:
|
||||
// If approval is required and fave is
|
||||
// preapproved, that means this is a fave
|
||||
// of one of our statuses with permission
|
||||
// that matched on a following/followers
|
||||
// collection. Do the Accept immediately and
|
||||
// then process everything else as normal,
|
||||
// sending out the Like with the approval
|
||||
// URI attached.
|
||||
|
||||
// Store an already-accepted interaction request.
|
||||
requestID := id.NewULID()
|
||||
approval := >smodel.InteractionRequest{
|
||||
ID: requestID,
|
||||
TargetStatusID: fave.StatusID,
|
||||
TargetAccountID: fave.TargetAccountID,
|
||||
TargetAccount: fave.TargetAccount,
|
||||
InteractingAccountID: fave.AccountID,
|
||||
InteractingAccount: fave.Account,
|
||||
InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(fave.URI, gtsmodel.LikeRequestSuffix),
|
||||
InteractionURI: fave.URI,
|
||||
InteractionType: gtsmodel.InteractionLike,
|
||||
Polite: util.Ptr(false), // TODO: Change this in v0.21.0 when we only send out polite requests.
|
||||
Like: fave,
|
||||
ResponseURI: uris.GenerateURIForAccept(fave.TargetAccount.Username, requestID),
|
||||
AuthorizationURI: uris.GenerateURIForAuthorization(fave.TargetAccount.Username, requestID),
|
||||
AcceptedAt: time.Now(),
|
||||
}
|
||||
if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil {
|
||||
return gtserror.Newf("db error putting pre-approved interaction request: %w", err)
|
||||
}
|
||||
|
||||
// Mark the fave itself as now approved.
|
||||
fave.PendingApproval = util.Ptr(false)
|
||||
fave.PreApproved = false
|
||||
fave.ApprovedByURI = approval.AuthorizationURI
|
||||
if err := p.state.DB.UpdateStatusFave(
|
||||
ctx,
|
||||
fave,
|
||||
"pending_approval",
|
||||
"approved_by_uri",
|
||||
); err != nil {
|
||||
return gtserror.Newf("db error updating status fave: %w", err)
|
||||
}
|
||||
|
||||
if err := p.federate.AcceptInteraction(ctx, approval); err != nil {
|
||||
return gtserror.Newf("error federating pre-approval of fave: %w", err)
|
||||
}
|
||||
|
||||
// Don't return, just continue as normal.
|
||||
}
|
||||
|
||||
if err := p.surface.notifyFave(ctx, fave); err != nil {
|
||||
log.Errorf(ctx, "error notifying fave: %v", err)
|
||||
}
|
||||
@@ -553,87 +492,205 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg *messages.FromClientAPI
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *clientAPI) CreateLikeRequest(ctx context.Context, cMsg *messages.FromClientAPI) error {
|
||||
fave, ok := cMsg.GTSModel.(*gtsmodel.StatusFave)
|
||||
if !ok {
|
||||
return gtserror.Newf("%T not parseable as *gtsmodel.StatusFave", cMsg.GTSModel)
|
||||
}
|
||||
|
||||
// Create a polite like request.
|
||||
intReqID := id.NewULIDFromTime(fave.CreatedAt)
|
||||
intReq := >smodel.InteractionRequest{
|
||||
ID: intReqID,
|
||||
TargetStatusID: fave.StatusID,
|
||||
TargetStatus: fave.Status,
|
||||
TargetAccountID: fave.TargetAccountID,
|
||||
TargetAccount: fave.TargetAccount,
|
||||
InteractingAccountID: fave.AccountID,
|
||||
InteractingAccount: fave.Account,
|
||||
InteractionRequestURI: uris.GenerateURIForLikeRequest(fave.Account.Username, intReqID),
|
||||
InteractionURI: fave.URI,
|
||||
InteractionType: gtsmodel.InteractionLike,
|
||||
Polite: util.Ptr(true),
|
||||
Like: fave,
|
||||
}
|
||||
|
||||
if !fave.PreApproved {
|
||||
// If the fave is not pre-approved, just
|
||||
// store the interaction request, notify
|
||||
// (local) target or federate request.
|
||||
if err := p.utils.storeInteractionRequest(
|
||||
ctx, intReq,
|
||||
); err != nil {
|
||||
return gtserror.Newf("error storing interaction request: %w", err)
|
||||
}
|
||||
|
||||
// Notify target account (if local) of pending fave.
|
||||
if err := p.surface.notifyPendingFave(ctx, intReq.Like); err != nil {
|
||||
return gtserror.Newf("error notifying pending fave: %w", err)
|
||||
}
|
||||
|
||||
// Send interaction request to target account (if remote).
|
||||
if err := p.federate.InteractionRequest(ctx, intReq); err != nil {
|
||||
return gtserror.Newf("error federating interaction request: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the fave is pre-approved, then it must
|
||||
// target a status on our instance, and the
|
||||
// faver gets automatic approval due to being
|
||||
// in the author's followers/following collection.
|
||||
//
|
||||
// Mark the interaction request as accepted, store it,
|
||||
// mark the interaction as approved, and then continue
|
||||
// with side effects as normal.
|
||||
|
||||
// Update intReq fields to
|
||||
// mark it as accepted.
|
||||
intReq.MarkAccepted()
|
||||
|
||||
// Put it in the DB.
|
||||
if err := p.utils.storeInteractionRequest(
|
||||
ctx, intReq,
|
||||
); err != nil {
|
||||
return gtserror.Newf("error storing interaction request: %w", err)
|
||||
}
|
||||
|
||||
// Mark the fave as now approved, referring to
|
||||
// the accepted interaction request we just stored.
|
||||
fave.PendingApproval = util.Ptr(false)
|
||||
fave.PreApproved = false
|
||||
fave.ApprovedByURI = intReq.AuthorizationURI
|
||||
if err := p.state.DB.UpdateStatusFave(
|
||||
ctx,
|
||||
fave,
|
||||
"pending_approval",
|
||||
"approved_by_uri",
|
||||
); err != nil {
|
||||
return gtserror.Newf("db error updating status fave: %w", err)
|
||||
}
|
||||
|
||||
// Send out the approval as an Accept.
|
||||
if err := p.federate.AcceptInteraction(ctx, intReq); err != nil {
|
||||
return gtserror.Newf("error federating pre-approval of fave: %w", err)
|
||||
}
|
||||
|
||||
// Notify the status author about the fave.
|
||||
if err := p.surface.notifyFave(ctx, fave); err != nil {
|
||||
log.Errorf(ctx, "error notifying fave: %v", err)
|
||||
}
|
||||
|
||||
// We don't (yet) federate Likes out
|
||||
// to anyone but the target of the like,
|
||||
// so there's no need to send it anywhere.
|
||||
// Just return.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg *messages.FromClientAPI) error {
|
||||
boost, ok := cMsg.GTSModel.(*gtsmodel.Status)
|
||||
if !ok {
|
||||
return gtserror.Newf("%T not parseable as *gtsmodel.Status", cMsg.GTSModel)
|
||||
}
|
||||
|
||||
// If pending approval is true then status must
|
||||
// boost a status (either one of ours or a
|
||||
// remote) that requires approval for the boost.
|
||||
pendingApproval := util.PtrOrZero(boost.PendingApproval)
|
||||
// Timeline and notify the boost wrapper status.
|
||||
if err := p.surface.timelineAndNotifyStatus(ctx, boost); err != nil {
|
||||
log.Errorf(ctx, "error timelining and notifying status: %v", err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case pendingApproval && !boost.PreApproved:
|
||||
// If approval is required and boost isn't
|
||||
// preapproved, then send out the Announce to
|
||||
// only the boosted account (if it's remote),
|
||||
// and/or notify the account that's being
|
||||
// interacted with (if it's local): they can
|
||||
// approve or deny the interaction later.
|
||||
if err := p.utils.impoliteAnnounceRequest(ctx, boost); err != nil {
|
||||
return gtserror.Newf("error pending boost: %w", err)
|
||||
}
|
||||
// Notify the boost target account (if local).
|
||||
if err := p.surface.notifyAnnounce(ctx, boost); err != nil {
|
||||
log.Errorf(ctx, "error notifying boost: %v", err)
|
||||
}
|
||||
|
||||
// Send Announce to *remote* account inbox ONLY.
|
||||
if err := p.federate.Announce(ctx, boost); err != nil {
|
||||
return gtserror.Newf("error federating pending Announce: %v", err)
|
||||
}
|
||||
// Send out the Announce.
|
||||
if err := p.federate.Announce(ctx, boost); err != nil {
|
||||
log.Errorf(ctx, "error federating announce: %v", err)
|
||||
}
|
||||
|
||||
// Return early.
|
||||
return nil
|
||||
return nil
|
||||
}
|
||||
|
||||
case pendingApproval && boost.PreApproved:
|
||||
// If approval is required and boost is
|
||||
// preapproved, that means this is a boost
|
||||
// of one of our statuses with permission
|
||||
// that matched on a following/followers
|
||||
// collection. Do the Accept immediately and
|
||||
// then process everything else as normal,
|
||||
// sending out the Create with the approval
|
||||
// URI attached.
|
||||
func (p *clientAPI) CreateAnnounceRequest(ctx context.Context, cMsg *messages.FromClientAPI) error {
|
||||
boost, ok := cMsg.GTSModel.(*gtsmodel.Status)
|
||||
if !ok {
|
||||
return gtserror.Newf("%T not parseable as *gtsmodel.Status", cMsg.GTSModel)
|
||||
}
|
||||
|
||||
// Store an already-accepted interaction request.
|
||||
requestID := id.NewULID()
|
||||
approval := >smodel.InteractionRequest{
|
||||
ID: requestID,
|
||||
TargetStatusID: boost.BoostOfID,
|
||||
TargetAccountID: boost.BoostOfAccountID,
|
||||
TargetAccount: boost.BoostOfAccount,
|
||||
InteractingAccountID: boost.AccountID,
|
||||
InteractingAccount: boost.Account,
|
||||
InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(boost.URI, gtsmodel.AnnounceRequestSuffix),
|
||||
InteractionURI: boost.URI,
|
||||
InteractionType: gtsmodel.InteractionAnnounce,
|
||||
Polite: util.Ptr(false), // TODO: Change this in v0.21.0 when we only send out polite requests.
|
||||
Announce: boost,
|
||||
ResponseURI: uris.GenerateURIForAccept(boost.BoostOfAccount.Username, requestID),
|
||||
AuthorizationURI: uris.GenerateURIForAuthorization(boost.BoostOfAccount.Username, requestID),
|
||||
AcceptedAt: time.Now(),
|
||||
}
|
||||
if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil {
|
||||
return gtserror.Newf("db error putting pre-approved interaction request: %w", err)
|
||||
}
|
||||
// Create a polite reply request.
|
||||
intReqID := id.NewULIDFromTime(boost.CreatedAt)
|
||||
intReq := >smodel.InteractionRequest{
|
||||
ID: intReqID,
|
||||
TargetStatusID: boost.BoostOfID,
|
||||
TargetStatus: boost.BoostOf,
|
||||
TargetAccountID: boost.BoostOfAccountID,
|
||||
TargetAccount: boost.BoostOfAccount,
|
||||
InteractingAccountID: boost.AccountID,
|
||||
InteractingAccount: boost.Account,
|
||||
InteractionRequestURI: uris.GenerateURIForAnnounceRequest(boost.Account.Username, intReqID),
|
||||
InteractionURI: boost.URI,
|
||||
InteractionType: gtsmodel.InteractionAnnounce,
|
||||
Polite: util.Ptr(true),
|
||||
Announce: boost,
|
||||
}
|
||||
|
||||
// Mark the boost itself as now approved.
|
||||
boost.PendingApproval = util.Ptr(false)
|
||||
boost.PreApproved = false
|
||||
boost.ApprovedByURI = approval.AuthorizationURI
|
||||
if err := p.state.DB.UpdateStatus(
|
||||
ctx,
|
||||
boost,
|
||||
"pending_approval",
|
||||
"approved_by_uri",
|
||||
if !boost.PreApproved {
|
||||
// If the boost is not pre-approved, just
|
||||
// store the interaction request, notify
|
||||
// (local) target or federate request.
|
||||
if err := p.utils.storeInteractionRequest(
|
||||
ctx, intReq,
|
||||
); err != nil {
|
||||
return gtserror.Newf("db error updating status: %w", err)
|
||||
return gtserror.Newf("error storing interaction request: %w", err)
|
||||
}
|
||||
|
||||
if err := p.federate.AcceptInteraction(ctx, approval); err != nil {
|
||||
return gtserror.Newf("error federating pre-approval of boost: %w", err)
|
||||
// Notify target account (if local) of pending announce.
|
||||
if err := p.surface.notifyPendingAnnounce(ctx, intReq.Announce); err != nil {
|
||||
return gtserror.Newf("error notifying pending announce: %w", err)
|
||||
}
|
||||
|
||||
// Don't return, just continue as normal.
|
||||
// Send interaction request to target account (if remote).
|
||||
if err := p.federate.InteractionRequest(ctx, intReq); err != nil {
|
||||
return gtserror.Newf("error federating interaction request: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the boost is pre-approved, then it must
|
||||
// target a status on our instance, and the
|
||||
// booster gets automatic approval due to being
|
||||
// in the author's followers/following collection.
|
||||
//
|
||||
// Mark the interaction request as accepted, store it,
|
||||
// mark the interaction as approved, and then continue
|
||||
// with side effects as normal.
|
||||
|
||||
// Update intReq fields to
|
||||
// mark it as accepted.
|
||||
intReq.MarkAccepted()
|
||||
|
||||
// Put it in the DB.
|
||||
if err := p.utils.storeInteractionRequest(
|
||||
ctx, intReq,
|
||||
); err != nil {
|
||||
return gtserror.Newf("error storing interaction request: %w", err)
|
||||
}
|
||||
|
||||
// Mark the status as now approved, referring to
|
||||
// the accepted interaction request we just stored.
|
||||
boost.PendingApproval = util.Ptr(false)
|
||||
boost.PreApproved = false
|
||||
boost.ApprovedByURI = intReq.AuthorizationURI
|
||||
if err := p.state.DB.UpdateStatus(
|
||||
ctx,
|
||||
boost,
|
||||
"pending_approval",
|
||||
"approved_by_uri",
|
||||
); err != nil {
|
||||
return gtserror.Newf("db error updating status: %w", err)
|
||||
}
|
||||
|
||||
// Timeline and notify the boost wrapper status.
|
||||
@@ -646,6 +703,12 @@ func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg *messages.FromClien
|
||||
log.Errorf(ctx, "error notifying boost: %v", err)
|
||||
}
|
||||
|
||||
// Send out the approval as an Accept.
|
||||
if err := p.federate.AcceptInteraction(ctx, intReq); err != nil {
|
||||
return gtserror.Newf("error federating pre-approval of boost: %w", err)
|
||||
}
|
||||
|
||||
// Send out the announce itself.
|
||||
if err := p.federate.Announce(ctx, boost); err != nil {
|
||||
log.Errorf(ctx, "error federating announce: %v", err)
|
||||
}
|
||||
|
||||
@@ -373,171 +373,6 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithNotification() {
|
||||
suite.checkWebPushed(testStructs.WebPushSender, receivingAccount.ID, gtsmodel.NotificationStatus)
|
||||
}
|
||||
|
||||
// Even with notifications on for a user, backfilling a status should not notify or timeline it.
|
||||
func (suite *FromClientAPITestSuite) TestProcessCreateBackfilledStatusWithNotification() {
|
||||
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||
defer testrig.TearDownTestStructs(testStructs)
|
||||
|
||||
var (
|
||||
ctx = suite.T().Context()
|
||||
postingAccount = suite.testAccounts["admin_account"]
|
||||
receivingAccount = suite.testAccounts["local_account_1"]
|
||||
testList = suite.testLists["local_account_1_list_1"]
|
||||
streams = suite.openStreams(ctx,
|
||||
testStructs.Processor,
|
||||
receivingAccount,
|
||||
[]string{testList.ID},
|
||||
)
|
||||
publicStream = streams[stream.TimelinePublic]
|
||||
homeStream = streams[stream.TimelineHome]
|
||||
listStream = streams[stream.TimelineList+":"+testList.ID]
|
||||
notifStream = streams[stream.TimelineNotifications]
|
||||
|
||||
// Admin account posts a new top-level status.
|
||||
status = suite.newStatus(
|
||||
ctx,
|
||||
testStructs.State,
|
||||
postingAccount,
|
||||
gtsmodel.VisibilityPublic,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
nil,
|
||||
)
|
||||
)
|
||||
|
||||
// Update the follow from receiving account -> posting account so
|
||||
// that receiving account wants notifs when posting account posts.
|
||||
follow := new(gtsmodel.Follow)
|
||||
*follow = *suite.testFollows["local_account_1_admin_account"]
|
||||
|
||||
follow.Notify = util.Ptr(true)
|
||||
if err := testStructs.State.DB.UpdateFollow(ctx, follow); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Process the new status as a backfill.
|
||||
if err := testStructs.Processor.Workers().ProcessFromClientAPI(
|
||||
ctx,
|
||||
&messages.FromClientAPI{
|
||||
APObjectType: ap.ObjectNote,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: >smodel.BackfillStatus{Status: status},
|
||||
Origin: postingAccount,
|
||||
},
|
||||
); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// There should be no message in public stream.
|
||||
suite.checkStreamed(
|
||||
publicStream,
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
)
|
||||
|
||||
// There should be no message in the home stream.
|
||||
suite.checkStreamed(
|
||||
homeStream,
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
)
|
||||
|
||||
// There should be no message in the list stream.
|
||||
suite.checkStreamed(
|
||||
listStream,
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
)
|
||||
|
||||
// No notification should appear for the status.
|
||||
if testrig.WaitFor(func() bool {
|
||||
var err error
|
||||
_, err = testStructs.State.DB.GetNotification(
|
||||
ctx,
|
||||
gtsmodel.NotificationStatus,
|
||||
receivingAccount.ID,
|
||||
postingAccount.ID,
|
||||
status.ID,
|
||||
)
|
||||
return err == nil
|
||||
}) {
|
||||
suite.FailNow("a status notification was created, but should not have been")
|
||||
}
|
||||
|
||||
// There should be no message in the notification stream.
|
||||
suite.checkStreamed(
|
||||
notifStream,
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
)
|
||||
|
||||
// There should be no Web Push status notification.
|
||||
suite.checkNotWebPushed(testStructs.WebPushSender, receivingAccount.ID)
|
||||
}
|
||||
|
||||
// Backfilled statuses should not federate when created.
|
||||
func (suite *FromClientAPITestSuite) TestProcessCreateBackfilledStatusWithRemoteFollower() {
|
||||
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||
defer testrig.TearDownTestStructs(testStructs)
|
||||
|
||||
var (
|
||||
ctx = suite.T().Context()
|
||||
postingAccount = suite.testAccounts["local_account_1"]
|
||||
receivingAccount = suite.testAccounts["remote_account_1"]
|
||||
|
||||
// Local account posts a new top-level status.
|
||||
status = suite.newStatus(
|
||||
ctx,
|
||||
testStructs.State,
|
||||
postingAccount,
|
||||
gtsmodel.VisibilityPublic,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
nil,
|
||||
)
|
||||
)
|
||||
|
||||
// Follow the local account from the remote account.
|
||||
follow := >smodel.Follow{
|
||||
ID: "01JJHW9RW28SC1NEPZ0WBJQ4ZK",
|
||||
CreatedAt: testrig.TimeMustParse("2022-05-14T13:21:09+02:00"),
|
||||
UpdatedAt: testrig.TimeMustParse("2022-05-14T13:21:09+02:00"),
|
||||
AccountID: receivingAccount.ID,
|
||||
TargetAccountID: postingAccount.ID,
|
||||
ShowReblogs: util.Ptr(true),
|
||||
URI: "http://fossbros-anonymous.io/users/foss_satan/follow/01JJHWEVC7F8W2JDW1136K431K",
|
||||
Notify: util.Ptr(false),
|
||||
}
|
||||
|
||||
if err := testStructs.State.DB.PutFollow(ctx, follow); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Process the new status as a backfill.
|
||||
if err := testStructs.Processor.Workers().ProcessFromClientAPI(
|
||||
ctx,
|
||||
&messages.FromClientAPI{
|
||||
APObjectType: ap.ObjectNote,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: >smodel.BackfillStatus{Status: status},
|
||||
Origin: postingAccount,
|
||||
},
|
||||
); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// No deliveries should be queued.
|
||||
suite.Zero(testStructs.State.Workers.Delivery.Queue.Len())
|
||||
}
|
||||
|
||||
func (suite *FromClientAPITestSuite) TestProcessCreateStatusReply() {
|
||||
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||
defer testrig.TearDownTestStructs(testStructs)
|
||||
|
||||
@@ -303,56 +303,67 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
|
||||
// If pending approval is true then
|
||||
// status must reply to a LOCAL status
|
||||
// that requires approval for the reply.
|
||||
pendingApproval := util.PtrOrZero(status.PendingApproval)
|
||||
|
||||
switch {
|
||||
case pendingApproval && !status.PreApproved:
|
||||
// If approval is required and status isn't
|
||||
// preapproved, then just notify the account
|
||||
// that's being interacted with: they can
|
||||
// approve or deny the interaction later.
|
||||
if err := p.utils.impoliteReplyRequest(ctx, status); err != nil {
|
||||
return gtserror.Newf("error pending reply: %w", err)
|
||||
}
|
||||
|
||||
// Return early.
|
||||
return nil
|
||||
|
||||
case pendingApproval && status.PreApproved:
|
||||
// If approval is required and status is
|
||||
// preapproved, that means this is a reply
|
||||
// to one of our statuses with permission
|
||||
// that matched on a following/followers
|
||||
// collection. Do the Accept immediately and
|
||||
// then process everything else as normal.
|
||||
|
||||
// Store an already-accepted
|
||||
// impolite interaction request.
|
||||
requestID := id.NewULID()
|
||||
approval := >smodel.InteractionRequest{
|
||||
ID: requestID,
|
||||
if util.PtrOrZero(status.PendingApproval) {
|
||||
intReqID := id.NewULIDFromTime(status.CreatedAt)
|
||||
intReq := >smodel.InteractionRequest{
|
||||
ID: intReqID,
|
||||
TargetStatusID: status.InReplyToID,
|
||||
TargetStatus: status.InReplyTo,
|
||||
TargetAccountID: status.InReplyToAccountID,
|
||||
TargetAccount: status.InReplyToAccount,
|
||||
InteractingAccountID: status.AccountID,
|
||||
InteractingAccount: status.Account,
|
||||
InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(status.URI, gtsmodel.ReplyRequestSuffix),
|
||||
InteractionRequestURI: status.URI + gtsmodel.ImpoliteReplyRequestSuffix,
|
||||
InteractionURI: status.URI,
|
||||
InteractionType: gtsmodel.InteractionReply,
|
||||
Polite: util.Ptr(false),
|
||||
Reply: status,
|
||||
ResponseURI: uris.GenerateURIForAccept(status.InReplyToAccount.Username, requestID),
|
||||
AuthorizationURI: uris.GenerateURIForAuthorization(status.InReplyToAccount.Username, requestID),
|
||||
AcceptedAt: time.Now(),
|
||||
}
|
||||
if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil {
|
||||
return gtserror.Newf("db error putting pre-approved interaction request: %w", err)
|
||||
}
|
||||
|
||||
// Mark the status as now approved.
|
||||
status.PendingApproval = util.Ptr(false)
|
||||
if !status.PreApproved {
|
||||
// If approval is required and reply isn't
|
||||
// preapproved, just store the interaction request
|
||||
// and notify the account that's being interacted
|
||||
// with, they can handle the interaction later.
|
||||
if err := p.utils.storeInteractionRequest(
|
||||
ctx, intReq,
|
||||
); err != nil {
|
||||
return gtserror.Newf("error storing interaction request: %w", err)
|
||||
}
|
||||
|
||||
// Notify target account (if local) of pending reply.
|
||||
if err := p.surface.notifyPendingReply(ctx, intReq.Reply); err != nil {
|
||||
return gtserror.Newf("error notifying pending reply: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// If approval is required and status *is* preapproved,
|
||||
// that means this is a reply to one of our statuses
|
||||
// that was allowed based on replier's presence in a
|
||||
// following/followers collection.
|
||||
//
|
||||
// Mark the interaction request as accepted, store it,
|
||||
// mark the interaction as approved, and then continue
|
||||
// with side effects as normal.
|
||||
|
||||
// Update intReq fields to
|
||||
// mark it as accepted.
|
||||
intReq.MarkAccepted()
|
||||
|
||||
// Put it in the DB.
|
||||
if err := p.utils.storeInteractionRequest(
|
||||
ctx, intReq,
|
||||
); err != nil {
|
||||
return gtserror.Newf("error storing interaction request: %w", err)
|
||||
}
|
||||
|
||||
// Mark the status as now approved, referring to
|
||||
// the accepted interaction request we just stored.
|
||||
status.PreApproved = false
|
||||
status.ApprovedByURI = approval.AuthorizationURI
|
||||
status.PendingApproval = util.Ptr(false)
|
||||
status.ApprovedByURI = intReq.AuthorizationURI
|
||||
if err := p.state.DB.UpdateStatus(
|
||||
ctx,
|
||||
status,
|
||||
@@ -363,11 +374,12 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
|
||||
}
|
||||
|
||||
// Send out the approval as Accept.
|
||||
if err := p.federate.AcceptInteraction(ctx, approval); err != nil {
|
||||
if err := p.federate.AcceptInteraction(ctx, intReq); err != nil {
|
||||
return gtserror.Newf("error federating pre-approval of reply: %w", err)
|
||||
}
|
||||
|
||||
// Don't return, just continue as normal.
|
||||
// Don't return, just continue
|
||||
// side effects as normal.
|
||||
}
|
||||
|
||||
if err := p.surface.timelineAndNotifyStatus(ctx, status); err != nil {
|
||||
@@ -712,56 +724,67 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) er
|
||||
// If pending approval is true then
|
||||
// fave must target a LOCAL status
|
||||
// that requires approval for the fave.
|
||||
pendingApproval := util.PtrOrZero(fave.PendingApproval)
|
||||
|
||||
switch {
|
||||
case pendingApproval && !fave.PreApproved:
|
||||
// If approval is required and fave isn't
|
||||
// preapproved, then just notify the account
|
||||
// that's being interacted with: they can
|
||||
// approve or deny the interaction later.
|
||||
if err := p.utils.impoliteFaveRequest(ctx, fave); err != nil {
|
||||
return gtserror.Newf("error pending fave: %w", err)
|
||||
}
|
||||
|
||||
// Return early.
|
||||
return nil
|
||||
|
||||
case pendingApproval && fave.PreApproved:
|
||||
// If approval is required and fave is
|
||||
// preapproved, that means this is a fave
|
||||
// of one of our statuses with permission
|
||||
// that matched on a following/followers
|
||||
// collection. Do the Accept immediately and
|
||||
// then process everything else as normal.
|
||||
|
||||
// Store an already-accepted
|
||||
// impolite interaction request.
|
||||
requestID := id.NewULID()
|
||||
approval := >smodel.InteractionRequest{
|
||||
ID: requestID,
|
||||
if util.PtrOrZero(fave.PendingApproval) {
|
||||
intReqID := id.NewULIDFromTime(fave.CreatedAt)
|
||||
intReq := >smodel.InteractionRequest{
|
||||
ID: intReqID,
|
||||
TargetStatusID: fave.StatusID,
|
||||
TargetStatus: fave.Status,
|
||||
TargetAccountID: fave.TargetAccountID,
|
||||
TargetAccount: fave.TargetAccount,
|
||||
InteractingAccountID: fave.AccountID,
|
||||
InteractingAccount: fave.Account,
|
||||
InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(fave.URI, gtsmodel.LikeRequestSuffix),
|
||||
InteractionRequestURI: fave.URI + gtsmodel.ImpoliteLikeRequestSuffix,
|
||||
InteractionURI: fave.URI,
|
||||
InteractionType: gtsmodel.InteractionLike,
|
||||
Polite: util.Ptr(false),
|
||||
Like: fave,
|
||||
ResponseURI: uris.GenerateURIForAccept(fave.TargetAccount.Username, requestID),
|
||||
AuthorizationURI: uris.GenerateURIForAuthorization(fave.TargetAccount.Username, requestID),
|
||||
AcceptedAt: time.Now(),
|
||||
}
|
||||
if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil {
|
||||
return gtserror.Newf("db error putting pre-approved interaction request: %w", err)
|
||||
}
|
||||
|
||||
// Mark the fave itself as now approved.
|
||||
if !fave.PreApproved {
|
||||
// If approval is required and status fave isn't
|
||||
// preapproved, just store the interaction request
|
||||
// and notify the account that's being interacted
|
||||
// with, they can handle the interaction later.
|
||||
if err := p.utils.storeInteractionRequest(
|
||||
ctx, intReq,
|
||||
); err != nil {
|
||||
return gtserror.Newf("error storing interaction request: %w", err)
|
||||
}
|
||||
|
||||
// Notify target account (if local) of pending like.
|
||||
if err := p.surface.notifyPendingFave(ctx, intReq.Like); err != nil {
|
||||
return gtserror.Newf("error notifying pending fave: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// If approval is required and fave *is* preapproved,
|
||||
// that means this is a fave of one of our statuses
|
||||
// that was allowed based on faver's presence in a
|
||||
// following/followers collection.
|
||||
//
|
||||
// Mark the interaction request as accepted, store it,
|
||||
// mark the interaction as approved, and then continue
|
||||
// with side effects as normal.
|
||||
|
||||
// Update intReq fields to
|
||||
// mark it as accepted.
|
||||
intReq.MarkAccepted()
|
||||
|
||||
// Put it in the DB.
|
||||
if err := p.utils.storeInteractionRequest(
|
||||
ctx, intReq,
|
||||
); err != nil {
|
||||
return gtserror.Newf("error storing interaction request: %w", err)
|
||||
}
|
||||
|
||||
// Mark the fave as now approved, referring to
|
||||
// the accepted interaction request we just stored.
|
||||
fave.PendingApproval = util.Ptr(false)
|
||||
fave.PreApproved = false
|
||||
fave.ApprovedByURI = approval.AuthorizationURI
|
||||
fave.ApprovedByURI = intReq.AuthorizationURI
|
||||
if err := p.state.DB.UpdateStatusFave(
|
||||
ctx,
|
||||
fave,
|
||||
@@ -772,11 +795,12 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) er
|
||||
}
|
||||
|
||||
// Send out the approval as Accept.
|
||||
if err := p.federate.AcceptInteraction(ctx, approval); err != nil {
|
||||
if err := p.federate.AcceptInteraction(ctx, intReq); err != nil {
|
||||
return gtserror.Newf("error federating pre-approval of fave: %w", err)
|
||||
}
|
||||
|
||||
// Don't return, just continue as normal.
|
||||
// Don't return, just continue
|
||||
// side effects as normal.
|
||||
}
|
||||
|
||||
if err := p.surface.notifyFave(ctx, fave); err != nil {
|
||||
@@ -896,56 +920,66 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
|
||||
// If pending approval is true then
|
||||
// boost must target a LOCAL status
|
||||
// that requires approval for the boost.
|
||||
pendingApproval := util.PtrOrZero(boost.PendingApproval)
|
||||
|
||||
switch {
|
||||
case pendingApproval && !boost.PreApproved:
|
||||
// If approval is required and boost isn't
|
||||
// preapproved, then just notify the account
|
||||
// that's being interacted with: they can
|
||||
// approve or deny the interaction later.
|
||||
if err := p.utils.impoliteAnnounceRequest(ctx, boost); err != nil {
|
||||
return gtserror.Newf("error pending boost: %w", err)
|
||||
}
|
||||
|
||||
// Return early.
|
||||
return nil
|
||||
|
||||
case pendingApproval && boost.PreApproved:
|
||||
// If approval is required and status is
|
||||
// preapproved, that means this is a boost
|
||||
// of one of our statuses with permission
|
||||
// that matched on a following/followers
|
||||
// collection. Do the Accept immediately and
|
||||
// then process everything else as normal.
|
||||
|
||||
// Store an already-accepted
|
||||
// impolite interaction request.
|
||||
requestID := id.NewULID()
|
||||
approval := >smodel.InteractionRequest{
|
||||
ID: requestID,
|
||||
if util.PtrOrZero(boost.PendingApproval) {
|
||||
intReqID := id.NewULIDFromTime(boost.CreatedAt)
|
||||
intReq := >smodel.InteractionRequest{
|
||||
ID: intReqID,
|
||||
TargetStatusID: boost.BoostOfID,
|
||||
TargetStatus: boost.BoostOf,
|
||||
TargetAccountID: boost.BoostOfAccountID,
|
||||
TargetAccount: boost.BoostOfAccount,
|
||||
InteractingAccountID: boost.AccountID,
|
||||
InteractingAccount: boost.Account,
|
||||
InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(boost.URI, gtsmodel.AnnounceRequestSuffix),
|
||||
InteractionRequestURI: boost.URI + gtsmodel.ImpoliteAnnounceRequestSuffix,
|
||||
InteractionURI: boost.URI,
|
||||
InteractionType: gtsmodel.InteractionAnnounce,
|
||||
Polite: util.Ptr(false),
|
||||
Announce: boost,
|
||||
ResponseURI: uris.GenerateURIForAccept(boost.BoostOfAccount.Username, requestID),
|
||||
AuthorizationURI: uris.GenerateURIForAuthorization(boost.BoostOfAccount.Username, requestID),
|
||||
AcceptedAt: time.Now(),
|
||||
}
|
||||
if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil {
|
||||
return gtserror.Newf("db error putting pre-approved interaction request: %w", err)
|
||||
|
||||
if !boost.PreApproved {
|
||||
// If approval is required and announce isn't
|
||||
// preapproved, just store the interaction request
|
||||
// and notify the account that's being interacted
|
||||
// with, they can handle the interaction later.
|
||||
if err := p.utils.storeInteractionRequest(
|
||||
ctx, intReq,
|
||||
); err != nil {
|
||||
return gtserror.Newf("error storing interaction request: %w", err)
|
||||
}
|
||||
|
||||
// Notify target account (if local) of pending announce.
|
||||
if err := p.surface.notifyPendingAnnounce(ctx, intReq.Announce); err != nil {
|
||||
return gtserror.Newf("error notifying pending announce: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// If approval is required and boost *is* preapproved,
|
||||
// that means this is a boost of one of our statuses
|
||||
// that was allowed based on booster's presence in a
|
||||
// following/followers collection.
|
||||
//
|
||||
// Mark the interaction request as accepted, store it,
|
||||
// mark the interaction as approved, and then continue
|
||||
// with side effects as normal.
|
||||
|
||||
// Update intReq fields to
|
||||
// mark it as accepted.
|
||||
intReq.MarkAccepted()
|
||||
|
||||
// Put it in the DB.
|
||||
if err := p.utils.storeInteractionRequest(
|
||||
ctx, intReq,
|
||||
); err != nil {
|
||||
return gtserror.Newf("error storing interaction request: %w", err)
|
||||
}
|
||||
|
||||
// Mark the boost itself as now approved.
|
||||
boost.PendingApproval = util.Ptr(false)
|
||||
boost.PreApproved = false
|
||||
boost.ApprovedByURI = approval.AuthorizationURI
|
||||
boost.ApprovedByURI = intReq.AuthorizationURI
|
||||
if err := p.state.DB.UpdateStatus(
|
||||
ctx,
|
||||
boost,
|
||||
@@ -956,11 +990,12 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
|
||||
}
|
||||
|
||||
// Send out the approval as Accept.
|
||||
if err := p.federate.AcceptInteraction(ctx, approval); err != nil {
|
||||
if err := p.federate.AcceptInteraction(ctx, intReq); err != nil {
|
||||
return gtserror.Newf("error federating pre-approval of boost: %w", err)
|
||||
}
|
||||
|
||||
// Don't return, just continue as normal.
|
||||
// Don't return, just continue
|
||||
// side effects as normal.
|
||||
}
|
||||
|
||||
// Timeline and notify the announce.
|
||||
@@ -1190,9 +1225,9 @@ func (p *fediAPI) AcceptReply(ctx context.Context, fMsg *messages.FromFediAPI) e
|
||||
log.Errorf(ctx, "error timelining and notifying status: %v", err)
|
||||
}
|
||||
|
||||
// Send out the reply again, fully this time.
|
||||
// Send out the reply.
|
||||
if err := p.federate.CreateStatus(ctx, reply); err != nil {
|
||||
log.Errorf(ctx, "error federating announce: %v", err)
|
||||
log.Errorf(ctx, "error federating status: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -1277,7 +1312,7 @@ func (p *fediAPI) AcceptPoliteReplyRequest(ctx context.Context, fMsg *messages.F
|
||||
|
||||
// Send out the reply with approval attached.
|
||||
if err := p.federate.CreateStatus(ctx, reply); err != nil {
|
||||
log.Errorf(ctx, "error federating announce: %v", err)
|
||||
log.Errorf(ctx, "error federating status: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -284,29 +284,19 @@ func (u *utils) redirectFollowers(
|
||||
return true
|
||||
}
|
||||
|
||||
// impoliteFaveRequest stores an interaction request
|
||||
// for the given fave, and notifies the interactee.
|
||||
//
|
||||
// It should be used only when an actor has sent a Like
|
||||
// directly in response to a post that requires approval
|
||||
// for it, instead of sending a LikeRequest.
|
||||
func (u *utils) impoliteFaveRequest(
|
||||
// storeInteractionRequest ensures that
|
||||
// the given interaction request for the
|
||||
// given interaction is stored in the db.
|
||||
func (u *utils) storeInteractionRequest(
|
||||
ctx context.Context,
|
||||
fave *gtsmodel.StatusFave,
|
||||
intReq *gtsmodel.InteractionRequest,
|
||||
) error {
|
||||
// Only create interaction request
|
||||
// if fave targets a local status.
|
||||
if fave.Status == nil ||
|
||||
!fave.Status.IsLocal() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Lock on the interaction URI.
|
||||
unlock := u.state.ProcessingLocks.Lock(fave.URI)
|
||||
unlock := u.state.ProcessingLocks.Lock(intReq.InteractionURI)
|
||||
defer unlock()
|
||||
|
||||
// Ensure no req with this URI exists already.
|
||||
req, err := u.state.DB.GetInteractionRequestByInteractionURI(ctx, fave.URI)
|
||||
req, err := u.state.DB.GetInteractionRequestByInteractionURI(ctx, intReq.InteractionURI)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
return gtserror.Newf("db error checking for existing interaction request: %w", err)
|
||||
}
|
||||
@@ -317,110 +307,10 @@ func (u *utils) impoliteFaveRequest(
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create + store new impolite interaction request.
|
||||
req = typeutils.StatusFaveToImpoliteInteractionRequest(fave)
|
||||
if err := u.state.DB.PutInteractionRequest(ctx, req); err != nil {
|
||||
// Store interaction request.
|
||||
if err := u.state.DB.PutInteractionRequest(ctx, intReq); err != nil {
|
||||
return gtserror.Newf("db error storing interaction request: %w", err)
|
||||
}
|
||||
|
||||
// Notify *local* account of pending fave.
|
||||
if err := u.surface.notifyPendingFave(ctx, fave); err != nil {
|
||||
return gtserror.Newf("error notifying pending fave: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// impoliteReplyRequest stores an interaction request
|
||||
// for the given reply, and notifies the interactee.
|
||||
//
|
||||
// It should be used only when an actor has sent a reply
|
||||
// directly in response to a post that requires approval
|
||||
// for it, instead of sending a ReplyRequest.
|
||||
func (u *utils) impoliteReplyRequest(
|
||||
ctx context.Context,
|
||||
reply *gtsmodel.Status,
|
||||
) error {
|
||||
// Only create interaction request if
|
||||
// status replies to a local status.
|
||||
if reply.InReplyTo == nil ||
|
||||
!reply.InReplyTo.IsLocal() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Lock on the interaction URI.
|
||||
unlock := u.state.ProcessingLocks.Lock(reply.URI)
|
||||
defer unlock()
|
||||
|
||||
// Ensure no req with this URI exists already.
|
||||
req, err := u.state.DB.GetInteractionRequestByInteractionURI(ctx, reply.URI)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
return gtserror.Newf("db error checking for existing interaction request: %w", err)
|
||||
}
|
||||
|
||||
if req != nil {
|
||||
// Interaction req already exists,
|
||||
// no need to do anything else.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create + store impolite interaction request.
|
||||
req = typeutils.StatusToImpoliteInteractionRequest(reply)
|
||||
if err := u.state.DB.PutInteractionRequest(ctx, req); err != nil {
|
||||
return gtserror.Newf("db error storing interaction request: %w", err)
|
||||
}
|
||||
|
||||
// Notify *local* account of pending reply.
|
||||
if err := u.surface.notifyPendingReply(ctx, reply); err != nil {
|
||||
return gtserror.Newf("error notifying pending reply: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// impoliteAnnounceRequest stores an interaction request
|
||||
// for the given announce, and notifies the interactee.
|
||||
//
|
||||
// It should be used only when an actor has sent an Announce
|
||||
// directly in response to a post that requires approval
|
||||
// for it, instead of sending an AnnounceRequest.
|
||||
func (u *utils) impoliteAnnounceRequest(
|
||||
ctx context.Context,
|
||||
boost *gtsmodel.Status,
|
||||
) error {
|
||||
// Only create interaction request if
|
||||
// status announces a local status.
|
||||
if boost.BoostOf == nil ||
|
||||
!boost.BoostOf.IsLocal() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Lock on the interaction URI.
|
||||
unlock := u.state.ProcessingLocks.Lock(boost.URI)
|
||||
defer unlock()
|
||||
|
||||
// Ensure no req with this URI exists already.
|
||||
req, err := u.state.DB.GetInteractionRequestByInteractionURI(ctx, boost.URI)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
return gtserror.Newf("db error checking for existing interaction request: %w", err)
|
||||
}
|
||||
|
||||
if req != nil {
|
||||
// Interaction req already exists,
|
||||
// no need to do anything else.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create + store impolite interaction request.
|
||||
req = typeutils.StatusToImpoliteInteractionRequest(boost)
|
||||
if err := u.state.DB.PutInteractionRequest(ctx, req); err != nil {
|
||||
return gtserror.Newf("db error storing interaction request: %w", err)
|
||||
}
|
||||
|
||||
// Notify *local* account of pending announce.
|
||||
if err := u.surface.notifyPendingAnnounce(ctx, boost); err != nil {
|
||||
return gtserror.Newf("error notifying pending announce: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -101,75 +101,6 @@ func (c *Converter) StatusToBoost(
|
||||
return boost, nil
|
||||
}
|
||||
|
||||
func StatusToImpoliteInteractionRequest(status *gtsmodel.Status) *gtsmodel.InteractionRequest {
|
||||
reqID := id.NewULIDFromTime(status.CreatedAt)
|
||||
|
||||
var (
|
||||
targetID string
|
||||
target *gtsmodel.Status
|
||||
targetAccountID string
|
||||
targetAccount *gtsmodel.Account
|
||||
interactionRequestURI string
|
||||
interactionType gtsmodel.InteractionType
|
||||
reply *gtsmodel.Status
|
||||
announce *gtsmodel.Status
|
||||
)
|
||||
|
||||
if status.InReplyToID != "" {
|
||||
// It's a reply.
|
||||
targetID = status.InReplyToID
|
||||
target = status.InReplyTo
|
||||
targetAccountID = status.InReplyToAccountID
|
||||
targetAccount = status.InReplyToAccount
|
||||
interactionRequestURI = gtsmodel.ForwardCompatibleInteractionRequestURI(status.URI, gtsmodel.ReplyRequestSuffix)
|
||||
interactionType = gtsmodel.InteractionReply
|
||||
reply = status
|
||||
} else {
|
||||
// It's a boost.
|
||||
targetID = status.BoostOfID
|
||||
target = status.BoostOf
|
||||
targetAccountID = status.BoostOfAccountID
|
||||
targetAccount = status.BoostOfAccount
|
||||
interactionRequestURI = gtsmodel.ForwardCompatibleInteractionRequestURI(status.URI, gtsmodel.AnnounceRequestSuffix)
|
||||
interactionType = gtsmodel.InteractionAnnounce
|
||||
announce = status
|
||||
}
|
||||
|
||||
return >smodel.InteractionRequest{
|
||||
ID: reqID,
|
||||
TargetStatusID: targetID,
|
||||
TargetStatus: target,
|
||||
TargetAccountID: targetAccountID,
|
||||
TargetAccount: targetAccount,
|
||||
InteractingAccountID: status.AccountID,
|
||||
InteractingAccount: status.Account,
|
||||
InteractionRequestURI: interactionRequestURI,
|
||||
InteractionURI: status.URI,
|
||||
InteractionType: interactionType,
|
||||
Polite: util.Ptr(false),
|
||||
Reply: reply,
|
||||
Announce: announce,
|
||||
}
|
||||
}
|
||||
|
||||
func StatusFaveToImpoliteInteractionRequest(fave *gtsmodel.StatusFave) *gtsmodel.InteractionRequest {
|
||||
reqID := id.NewULIDFromTime(fave.CreatedAt)
|
||||
return >smodel.InteractionRequest{
|
||||
ID: reqID,
|
||||
TargetStatusID: fave.StatusID,
|
||||
TargetStatus: fave.Status,
|
||||
TargetAccountID: fave.TargetAccountID,
|
||||
TargetAccount: fave.TargetAccount,
|
||||
InteractingAccountID: fave.AccountID,
|
||||
InteractingAccount: fave.Account,
|
||||
InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(fave.URI, gtsmodel.LikeRequestSuffix),
|
||||
InteractionURI: fave.URI,
|
||||
InteractionType: gtsmodel.InteractionLike,
|
||||
Polite: util.Ptr(false),
|
||||
Like: fave,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Converter) StatusToSinBinStatus(
|
||||
ctx context.Context,
|
||||
status *gtsmodel.Status,
|
||||
|
||||
@@ -2462,3 +2462,108 @@ func (c *Converter) appendASInteractionAuthorization(
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InteractionReqToASInteractionRequestable converts the given
|
||||
// interaction request to either a LikeRequest, ReplyRequest,
|
||||
// or AnnounceRequest, with appropriate instrument and addressing.
|
||||
//
|
||||
// Result will look something like:
|
||||
//
|
||||
// {
|
||||
// "@context": [... blah blah blah ...],
|
||||
// "actor": "http://fossbros-anonymous.io/users/foss_satan",
|
||||
// "id": "https://fossbros-anonymous.io/users/foss_satan/interaction_requests/01J1AKRRHQ6MDDQHV0TP716T2K",
|
||||
// "instrument": { [... the note that foss_satan is replying with ...] },
|
||||
// "object": "http://localhost:8080/users/the_mighty_zork/statuses/01JJYCVKCXB9JTQD1XW2KB8MT3",
|
||||
// "to": "http://localhost:8080/users/the_mighty_zork",
|
||||
// "type": "ReplyRequest"
|
||||
// }
|
||||
func (c *Converter) InteractionReqToASInteractionRequestable(
|
||||
ctx context.Context,
|
||||
req *gtsmodel.InteractionRequest,
|
||||
) (ap.InteractionRequestable, error) {
|
||||
|
||||
// Actor of the interaction aka the interacting account.
|
||||
actorIRI, err := url.Parse(req.InteractingAccount.URI)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("invalid account uri: %w", err)
|
||||
}
|
||||
|
||||
// Object of the interaction aka the target status.
|
||||
objectIRI, err := url.Parse(req.TargetStatus.URI)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("invalid object uri: %w", err)
|
||||
}
|
||||
|
||||
// Account targeted by the interaction.
|
||||
toIRI, err := url.Parse(req.TargetAccount.URI)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("invalid to uri: %w", err)
|
||||
}
|
||||
|
||||
// Exact type of InteractionRequestable varies
|
||||
// depending on whether this is a LikeRequest,
|
||||
// ReplyRequest, or AnnounceRequest.
|
||||
var (
|
||||
v ap.InteractionRequestable
|
||||
instrumentProp = streams.NewActivityStreamsInstrumentProperty()
|
||||
)
|
||||
switch req.InteractionType {
|
||||
|
||||
// LikeRequest
|
||||
case gtsmodel.InteractionLike:
|
||||
v = streams.NewGoToSocialLikeRequest()
|
||||
like, err := c.FaveToAS(ctx, req.Like)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error converting like: %w", err)
|
||||
}
|
||||
instrumentProp.AppendActivityStreamsLike(like)
|
||||
|
||||
// ReplyRequest
|
||||
case gtsmodel.InteractionReply:
|
||||
v = streams.NewGoToSocialReplyRequest()
|
||||
statusable, err := c.StatusToAS(ctx, req.Reply)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error converting reply: %w", err)
|
||||
}
|
||||
|
||||
// If normal status, append as note.
|
||||
// If a poll, append as question.
|
||||
switch t := statusable.(type) {
|
||||
case vocab.ActivityStreamsNote:
|
||||
instrumentProp.AppendActivityStreamsNote(t)
|
||||
case vocab.ActivityStreamsQuestion:
|
||||
instrumentProp.AppendActivityStreamsQuestion(t)
|
||||
default:
|
||||
return nil, gtserror.Newf("type %T not supported as instrument of ReplyRequest", t)
|
||||
}
|
||||
|
||||
// AnnounceRequest
|
||||
case gtsmodel.InteractionAnnounce:
|
||||
v = streams.NewGoToSocialAnnounceRequest()
|
||||
announce, err := c.BoostToAS(ctx, req.Announce)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error converting announce: %w", err)
|
||||
}
|
||||
instrumentProp.AppendActivityStreamsAnnounce(announce)
|
||||
}
|
||||
|
||||
// Set ID.
|
||||
if err := ap.SetJSONLDIdStr(v, req.InteractionRequestURI); err != nil {
|
||||
return nil, gtserror.Newf("error setting id: %w", err)
|
||||
}
|
||||
|
||||
// Set actor IRI.
|
||||
ap.AppendActorIRIs(v, actorIRI)
|
||||
|
||||
// Set object IRI.
|
||||
ap.AppendObjectIRIs(v, objectIRI)
|
||||
|
||||
// Set to IRI.
|
||||
ap.AppendTo(v, toIRI)
|
||||
|
||||
// Set instrument.
|
||||
v.SetActivityStreamsInstrument(instrumentProp)
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
@@ -1516,7 +1516,7 @@ func (suite *InternalToASTestSuite) TestImpoliteInteractionReqToASAcceptAnnounce
|
||||
TargetAccount: acceptingAccount,
|
||||
InteractingAccountID: interactingAccount.ID,
|
||||
InteractingAccount: interactingAccount,
|
||||
InteractionRequestURI: "https://fossbros-anonymous.io/users/foss_satan/statuses/01J1AKRRHQ6MDDQHV0TP716T2K" + gtsmodel.AnnounceRequestSuffix,
|
||||
InteractionRequestURI: "https://fossbros-anonymous.io/users/foss_satan/statuses/01J1AKRRHQ6MDDQHV0TP716T2K" + gtsmodel.ImpoliteAnnounceRequestSuffix,
|
||||
InteractionURI: "https://fossbros-anonymous.io/users/foss_satan/statuses/01J1AKRRHQ6MDDQHV0TP716T2K",
|
||||
InteractionType: gtsmodel.InteractionAnnounce,
|
||||
Polite: util.Ptr(false),
|
||||
@@ -1569,7 +1569,7 @@ func (suite *InternalToASTestSuite) TestImpoliteInteractionReqToASAcceptLike() {
|
||||
TargetAccount: acceptingAccount,
|
||||
InteractingAccountID: interactingAccount.ID,
|
||||
InteractingAccount: interactingAccount,
|
||||
InteractionRequestURI: "https://fossbros-anonymous.io/users/foss_satan/likes/01J1AKRRHQ6MDDQHV0TP716T2K" + gtsmodel.LikeRequestSuffix,
|
||||
InteractionRequestURI: "https://fossbros-anonymous.io/users/foss_satan/likes/01J1AKRRHQ6MDDQHV0TP716T2K" + gtsmodel.ImpoliteLikeRequestSuffix,
|
||||
InteractionURI: "https://fossbros-anonymous.io/users/foss_satan/likes/01J1AKRRHQ6MDDQHV0TP716T2K",
|
||||
InteractionType: gtsmodel.InteractionLike,
|
||||
Polite: util.Ptr(false),
|
||||
@@ -1717,6 +1717,129 @@ func (suite *InternalToASTestSuite) TestPoliteInteractionReqToASAuthorization()
|
||||
}`, string(b))
|
||||
}
|
||||
|
||||
func (suite *InternalToASTestSuite) TestInteractionReqToASInteractionRequestable() {
|
||||
targetAccount := suite.testAccounts["local_account_1"]
|
||||
interactingAccount := suite.testAccounts["remote_account_1"]
|
||||
interactingStatus := suite.testStatuses["remote_account_1_status_1"]
|
||||
|
||||
req := >smodel.InteractionRequest{
|
||||
ID: "01J1AKMZ8JE5NW0ZSFTRC1JJNE",
|
||||
TargetStatusID: "01JJYCVKCXB9JTQD1XW2KB8MT3",
|
||||
TargetStatus: >smodel.Status{URI: "http://localhost:8080/users/the_mighty_zork/statuses/01JJYCVKCXB9JTQD1XW2KB8MT3"},
|
||||
TargetAccountID: targetAccount.ID,
|
||||
TargetAccount: targetAccount,
|
||||
InteractingAccountID: interactingAccount.ID,
|
||||
InteractingAccount: interactingAccount,
|
||||
InteractionRequestURI: "https://fossbros-anonymous.io/users/foss_satan/interaction_requests/01J1AKRRHQ6MDDQHV0TP716T2K",
|
||||
InteractionURI: "https://fossbros-anonymous.io/users/foss_satan/statuses/01J1AKRRHQ6MDDQHV0TP716T2K",
|
||||
InteractionType: gtsmodel.InteractionReply,
|
||||
Reply: interactingStatus,
|
||||
Polite: util.Ptr(true),
|
||||
}
|
||||
|
||||
accept, err := suite.typeconverter.InteractionReqToASInteractionRequestable(
|
||||
suite.T().Context(),
|
||||
req,
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
i, err := ap.Serialize(accept)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(i, "", " ")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Equal(`{
|
||||
"@context": [
|
||||
"https://gotosocial.org/ns",
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
{
|
||||
"blurhash": "toot:blurhash",
|
||||
"sensitive": "as:sensitive",
|
||||
"toot": "http://joinmastodon.org/ns#"
|
||||
}
|
||||
],
|
||||
"actor": "http://fossbros-anonymous.io/users/foss_satan",
|
||||
"id": "https://fossbros-anonymous.io/users/foss_satan/interaction_requests/01J1AKRRHQ6MDDQHV0TP716T2K",
|
||||
"instrument": {
|
||||
"attachment": [
|
||||
{
|
||||
"blurhash": "L3Q9_@4n9E?axW4mD$Mx~q00Di%L",
|
||||
"mediaType": "image/jpeg",
|
||||
"name": "tweet from thoughts of dog: i drank. all the water. in my bowl. earlier. but just now. i returned. to the same bowl. and it was. full again.. the bowl. is haunted",
|
||||
"type": "Image",
|
||||
"url": "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg"
|
||||
}
|
||||
],
|
||||
"attributedTo": "http://fossbros-anonymous.io/users/foss_satan",
|
||||
"cc": "https://www.w3.org/ns/activitystreams#Public",
|
||||
"content": "\u003cp\u003edark souls status bot: \"thoughts of dog\"\u003c/p\u003e",
|
||||
"contentMap": {
|
||||
"en": "\u003cp\u003edark souls status bot: \"thoughts of dog\"\u003c/p\u003e"
|
||||
},
|
||||
"id": "http://fossbros-anonymous.io/users/foss_satan/statuses/01FVW7JHQFSFK166WWKR8CBA6M",
|
||||
"interactionPolicy": {
|
||||
"canAnnounce": {
|
||||
"always": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"approvalRequired": [],
|
||||
"automaticApproval": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"manualApproval": []
|
||||
},
|
||||
"canLike": {
|
||||
"always": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"approvalRequired": [],
|
||||
"automaticApproval": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"manualApproval": []
|
||||
},
|
||||
"canReply": {
|
||||
"always": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"approvalRequired": [],
|
||||
"automaticApproval": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"manualApproval": []
|
||||
}
|
||||
},
|
||||
"published": "2021-09-20T12:40:37+02:00",
|
||||
"replies": {
|
||||
"first": {
|
||||
"id": "http://fossbros-anonymous.io/users/foss_satan/statuses/01FVW7JHQFSFK166WWKR8CBA6M/replies?page=true",
|
||||
"next": "http://fossbros-anonymous.io/users/foss_satan/statuses/01FVW7JHQFSFK166WWKR8CBA6M/replies?page=true\u0026only_other_accounts=false",
|
||||
"partOf": "http://fossbros-anonymous.io/users/foss_satan/statuses/01FVW7JHQFSFK166WWKR8CBA6M/replies",
|
||||
"type": "CollectionPage"
|
||||
},
|
||||
"id": "http://fossbros-anonymous.io/users/foss_satan/statuses/01FVW7JHQFSFK166WWKR8CBA6M/replies",
|
||||
"type": "Collection"
|
||||
},
|
||||
"sensitive": false,
|
||||
"summary": "",
|
||||
"tag": [],
|
||||
"to": "http://fossbros-anonymous.io/users/foss_satan/followers",
|
||||
"type": "Note",
|
||||
"url": "http://fossbros-anonymous.io/@foss_satan/statuses/01FVW7JHQFSFK166WWKR8CBA6M"
|
||||
},
|
||||
"object": "http://localhost:8080/users/the_mighty_zork/statuses/01JJYCVKCXB9JTQD1XW2KB8MT3",
|
||||
"to": "http://localhost:8080/users/the_mighty_zork",
|
||||
"type": "ReplyRequest"
|
||||
}`, string(b))
|
||||
}
|
||||
|
||||
func TestInternalToASTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(InternalToASTestSuite))
|
||||
}
|
||||
|
||||
+67
-22
@@ -27,28 +27,31 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
UsersPath = "users" // UsersPath is for serving users info
|
||||
StatusesPath = "statuses" // StatusesPath is for serving statuses
|
||||
InboxPath = "inbox" // InboxPath represents the activitypub inbox location
|
||||
OutboxPath = "outbox" // OutboxPath represents the activitypub outbox location
|
||||
FollowersPath = "followers" // FollowersPath represents the activitypub followers location
|
||||
FollowingPath = "following" // FollowingPath represents the activitypub following location
|
||||
LikedPath = "liked" // LikedPath represents the activitypub liked location
|
||||
CollectionsPath = "collections" // CollectionsPath represents the activitypub collections location
|
||||
FeaturedPath = "featured" // FeaturedPath represents the activitypub featured location
|
||||
PublicKeyPath = "main-key" // PublicKeyPath is for serving an account's public key
|
||||
FollowPath = "follow" // FollowPath used to generate the URI for an individual follow or follow request
|
||||
UpdatePath = "updates" // UpdatePath is used to generate the URI for an account update
|
||||
BlocksPath = "blocks" // BlocksPath is used to generate the URI for a block
|
||||
MovesPath = "moves" // MovesPath is used to generate the URI for a move
|
||||
ReportsPath = "reports" // ReportsPath is used to generate the URI for a report/flag
|
||||
ConfirmEmailPath = "confirm_email" // ConfirmEmailPath is used to generate the URI for an email confirmation link
|
||||
FileserverPath = "fileserver" // FileserverPath is a path component for serving attachments + media
|
||||
EmojiPath = "emoji" // EmojiPath represents the activitypub emoji location
|
||||
TagsPath = "tags" // TagsPath represents the activitypub tags location
|
||||
AcceptsPath = "accepts" // AcceptsPath represents the activitypub Accept's location
|
||||
AuthorizationsPath = "authorizations" // AuthorizationsPath represents the location of an Authorization type such as LikeAuthorization, ReplyAuthorization, etc.
|
||||
RejectsPath = "rejects" // RejectsPath represents the activitypub Reject's location
|
||||
UsersPath = "users" // UsersPath is for serving users info
|
||||
StatusesPath = "statuses" // StatusesPath is for serving statuses
|
||||
InboxPath = "inbox" // InboxPath represents the activitypub inbox location
|
||||
OutboxPath = "outbox" // OutboxPath represents the activitypub outbox location
|
||||
FollowersPath = "followers" // FollowersPath represents the activitypub followers location
|
||||
FollowingPath = "following" // FollowingPath represents the activitypub following location
|
||||
LikedPath = "liked" // LikedPath represents the activitypub liked location
|
||||
CollectionsPath = "collections" // CollectionsPath represents the activitypub collections location
|
||||
FeaturedPath = "featured" // FeaturedPath represents the activitypub featured location
|
||||
PublicKeyPath = "main-key" // PublicKeyPath is for serving an account's public key
|
||||
FollowPath = "follow" // FollowPath used to generate the URI for an individual follow or follow request
|
||||
UpdatePath = "updates" // UpdatePath is used to generate the URI for an account update
|
||||
BlocksPath = "blocks" // BlocksPath is used to generate the URI for a block
|
||||
MovesPath = "moves" // MovesPath is used to generate the URI for a move
|
||||
ReportsPath = "reports" // ReportsPath is used to generate the URI for a report/flag
|
||||
ConfirmEmailPath = "confirm_email" // ConfirmEmailPath is used to generate the URI for an email confirmation link
|
||||
FileserverPath = "fileserver" // FileserverPath is a path component for serving attachments + media
|
||||
EmojiPath = "emoji" // EmojiPath represents the activitypub emoji location
|
||||
TagsPath = "tags" // TagsPath represents the activitypub tags location
|
||||
AcceptsPath = "accepts" // AcceptsPath represents the activitypub Accept's location
|
||||
AuthorizationsPath = "authorizations" // AuthorizationsPath represents the location of an Authorization type such as LikeAuthorization, ReplyAuthorization, etc.
|
||||
RejectsPath = "rejects" // RejectsPath represents the activitypub Reject's location
|
||||
LikeRequestsPath = "like_requests" // LikeRequestsPath is used to generate the URI for a LikeRequest.
|
||||
ReplyRequestsPath = "reply_requests" // ReplyRequestsPath is used to generate the URI for a ReplyRequest.
|
||||
AnnounceRequestsPath = "announce_requests" // LikeRequestsPath is used to generate the URI for an AnnounceRequest.
|
||||
)
|
||||
|
||||
// UserURIs contains a bunch of UserURIs
|
||||
@@ -226,6 +229,48 @@ func GenerateURIForAuthorization(username string, id string) string {
|
||||
)
|
||||
}
|
||||
|
||||
// GenerateURIForLikeRequest returns the AP URI for a new LikeRequest object,
|
||||
// Eg., https://example.org/users/whatever_user/like_requests/01F7XTH1QGBAPMGF49WJZ91XGC
|
||||
func GenerateURIForLikeRequest(username string, id string) string {
|
||||
proto := config.GetProtocol()
|
||||
host := config.GetHost()
|
||||
return buildURL4(proto,
|
||||
host,
|
||||
UsersPath,
|
||||
username,
|
||||
LikeRequestsPath,
|
||||
id,
|
||||
)
|
||||
}
|
||||
|
||||
// GenerateURIForReplyRequest returns the AP URI for a new ReplyRequest object,
|
||||
// Eg., https://example.org/users/whatever_user/reply_requests/01F7XTH1QGBAPMGF49WJZ91XGC
|
||||
func GenerateURIForReplyRequest(username string, id string) string {
|
||||
proto := config.GetProtocol()
|
||||
host := config.GetHost()
|
||||
return buildURL4(proto,
|
||||
host,
|
||||
UsersPath,
|
||||
username,
|
||||
ReplyRequestsPath,
|
||||
id,
|
||||
)
|
||||
}
|
||||
|
||||
// GenerateURIForAnnounceRequest returns the AP URI for a new AnnounceRequest object,
|
||||
// Eg., https://example.org/users/whatever_user/announce_requests/01F7XTH1QGBAPMGF49WJZ91XGC
|
||||
func GenerateURIForAnnounceRequest(username string, id string) string {
|
||||
proto := config.GetProtocol()
|
||||
host := config.GetHost()
|
||||
return buildURL4(proto,
|
||||
host,
|
||||
UsersPath,
|
||||
username,
|
||||
AnnounceRequestsPath,
|
||||
id,
|
||||
)
|
||||
}
|
||||
|
||||
// GenerateURIForReject returns the AP URI for a new Reject activity -- something like:
|
||||
// https://example.org/users/whatever_user/rejects/01F7XTH1QGBAPMGF49WJZ91XGC
|
||||
func GenerateURIForReject(username string, thisRejectID string) string {
|
||||
|
||||
@@ -20,7 +20,6 @@ package web
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.superseriousbusiness.org/gopkg/log"
|
||||
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
|
||||
@@ -67,7 +66,6 @@ func (m *Module) prepareProfile(c *gin.Context) *profile {
|
||||
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
|
||||
return nil
|
||||
}
|
||||
requestedUser = strings.ToLower(requestedUser)
|
||||
|
||||
// Check what type of content is being requested.
|
||||
// If we're getting an AP request on this endpoint
|
||||
|
||||
@@ -19,7 +19,6 @@ package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.superseriousbusiness.org/gopkg/log"
|
||||
@@ -56,11 +55,6 @@ func (m *Module) rssFeedGETHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Usernames on our instance will always be lowercase.
|
||||
//
|
||||
// todo: https://codeberg.org/superseriousbusiness/gotosocial/issues/1813
|
||||
username = strings.ToLower(username)
|
||||
|
||||
// Parse paging parameters from request.
|
||||
page, errWithCode := paging.ParseIDPage(c,
|
||||
1, // min limit
|
||||
|
||||
+1
-12
@@ -20,7 +20,6 @@ package web
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
|
||||
apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
|
||||
@@ -52,22 +51,12 @@ func (m *Module) threadGETHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
statusID, errWithCode := apiutil.ParseWebStatusID(c.Param(apiutil.WebStatusIDKey))
|
||||
statusID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// Normalize requested username + status ID:
|
||||
//
|
||||
// - Usernames on our instance are (currently) always lowercase.
|
||||
// - StatusIDs on our instance are (currently) always ULIDs.
|
||||
//
|
||||
// todo: Update this logic when different username patterns
|
||||
// are allowed, and/or when status slugs are introduced.
|
||||
requestedUser = strings.ToLower(requestedUser)
|
||||
statusID = strings.ToUpper(statusID)
|
||||
|
||||
// Check what type of content is being requested. If we're getting an AP
|
||||
// request on this endpoint we should render the AP representation instead.
|
||||
accept, err := apiutil.NegotiateAccept(c, apiutil.HTMLOrActivityPubHeaders...)
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ import (
|
||||
const (
|
||||
confirmEmailPath = "/" + uris.ConfirmEmailPath
|
||||
profileGroupPath = "/@:username"
|
||||
statusPath = "/statuses/:" + apiutil.WebStatusIDKey // leave out the '/@:username' prefix as this will be served within the profile group
|
||||
statusPath = "/statuses/:" + apiutil.IDKey // leave out the '/@:username' prefix as this will be served within the profile group
|
||||
tagsPath = "/tags/:" + apiutil.TagNameKey
|
||||
customCSSPath = profileGroupPath + "/custom.css"
|
||||
instanceCustomCSSPath = "/custom.css"
|
||||
|
||||
+53
-34
@@ -8,42 +8,61 @@ import (
|
||||
//
|
||||
// The Activity types provided in the streams package implement this.
|
||||
type Activity interface {
|
||||
// Activity is also a vocab.Type
|
||||
// Activity is also
|
||||
// a vocab.Type.
|
||||
vocab.Type
|
||||
// GetActivityStreamsActor returns the "actor" property if it exists, and
|
||||
// nil otherwise.
|
||||
|
||||
// GetActivityStreamsActor returns "actor"
|
||||
// if it exists, or nil if it doesn't.
|
||||
GetActivityStreamsActor() vocab.ActivityStreamsActorProperty
|
||||
// GetActivityStreamsAudience returns the "audience" property if it
|
||||
// exists, and nil otherwise.
|
||||
GetActivityStreamsAudience() vocab.ActivityStreamsAudienceProperty
|
||||
// GetActivityStreamsBcc returns the "bcc" property if it exists, and nil
|
||||
// otherwise.
|
||||
GetActivityStreamsBcc() vocab.ActivityStreamsBccProperty
|
||||
// GetActivityStreamsBto returns the "bto" property if it exists, and nil
|
||||
// otherwise.
|
||||
GetActivityStreamsBto() vocab.ActivityStreamsBtoProperty
|
||||
// GetActivityStreamsCc returns the "cc" property if it exists, and nil
|
||||
// otherwise.
|
||||
GetActivityStreamsCc() vocab.ActivityStreamsCcProperty
|
||||
// GetActivityStreamsTo returns the "to" property if it exists, and nil
|
||||
// otherwise.
|
||||
GetActivityStreamsTo() vocab.ActivityStreamsToProperty
|
||||
// GetActivityStreamsAttributedTo returns the "attributedTo" property if
|
||||
// it exists, and nil otherwise.
|
||||
GetActivityStreamsAttributedTo() vocab.ActivityStreamsAttributedToProperty
|
||||
// GetActivityStreamsObject returns the "object" property if it exists,
|
||||
// and nil otherwise.
|
||||
GetActivityStreamsObject() vocab.ActivityStreamsObjectProperty
|
||||
// SetActivityStreamsActor sets the "actor" property.
|
||||
// SetActivityStreamsActor sets "actor".
|
||||
SetActivityStreamsActor(i vocab.ActivityStreamsActorProperty)
|
||||
// SetActivityStreamsObject sets the "object" property.
|
||||
SetActivityStreamsObject(i vocab.ActivityStreamsObjectProperty)
|
||||
// SetActivityStreamsTo sets the "to" property.
|
||||
SetActivityStreamsTo(i vocab.ActivityStreamsToProperty)
|
||||
// SetActivityStreamsBto sets the "bto" property.
|
||||
SetActivityStreamsBto(i vocab.ActivityStreamsBtoProperty)
|
||||
// SetActivityStreamsBcc sets the "bcc" property.
|
||||
SetActivityStreamsBcc(i vocab.ActivityStreamsBccProperty)
|
||||
// SetActivityStreamsAttributedTo sets the "attributedTo" property.
|
||||
|
||||
// GetActivityStreamsAttributedTo returns "attributedTo"
|
||||
// if it exists, or nil if it doesn't.
|
||||
GetActivityStreamsAttributedTo() vocab.ActivityStreamsAttributedToProperty
|
||||
// SetActivityStreamsAttributedTo sets "attributedTo".
|
||||
SetActivityStreamsAttributedTo(i vocab.ActivityStreamsAttributedToProperty)
|
||||
|
||||
// GetActivityStreamsAudience returns "audience"
|
||||
// property if it exists, or nil if it doesn't.
|
||||
GetActivityStreamsAudience() vocab.ActivityStreamsAudienceProperty
|
||||
// SetActivityStreamsAudience sets "audience".
|
||||
SetActivityStreamsAudience(t vocab.ActivityStreamsAudienceProperty)
|
||||
|
||||
// GetActivityStreamsObject returns the "object"
|
||||
// property if it exists, or nil if it doesn't.
|
||||
GetActivityStreamsObject() vocab.ActivityStreamsObjectProperty
|
||||
// SetActivityStreamsObject sets "object".
|
||||
SetActivityStreamsObject(i vocab.ActivityStreamsObjectProperty)
|
||||
|
||||
// GetActivityStreamsInstrument returns the "instrument"
|
||||
// property if it exists, or nil if it doesn't.
|
||||
GetActivityStreamsInstrument() vocab.ActivityStreamsInstrumentProperty
|
||||
// SetActivityStreamsInstrument sets "instrument".
|
||||
SetActivityStreamsInstrument(i vocab.ActivityStreamsInstrumentProperty)
|
||||
|
||||
// GetActivityStreamsCc returns "cc"
|
||||
// if it exists, or nil if it doesn't.
|
||||
GetActivityStreamsCc() vocab.ActivityStreamsCcProperty
|
||||
// SetActivityStreamsCc sets "cc".
|
||||
SetActivityStreamsCc(i vocab.ActivityStreamsCcProperty)
|
||||
|
||||
// GetActivityStreamsTo returns "to"
|
||||
// if it exists, or nil if it doesn't.
|
||||
GetActivityStreamsTo() vocab.ActivityStreamsToProperty
|
||||
// SetActivityStreamsTo sets "to".
|
||||
SetActivityStreamsTo(i vocab.ActivityStreamsToProperty)
|
||||
|
||||
// GetActivityStreamsBto returns "bto"
|
||||
// if it exists, or nil if it doesn't.
|
||||
GetActivityStreamsBto() vocab.ActivityStreamsBtoProperty
|
||||
// SetActivityStreamsBto sets "bto".
|
||||
SetActivityStreamsBto(i vocab.ActivityStreamsBtoProperty)
|
||||
|
||||
// GetActivityStreamsBcc returns "bcc"
|
||||
// if it exists, or nil if it doesn't.
|
||||
GetActivityStreamsBcc() vocab.ActivityStreamsBccProperty
|
||||
// SetActivityStreamsBcc sets "bcc".
|
||||
SetActivityStreamsBcc(i vocab.ActivityStreamsBccProperty)
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
# code.superseriousbusiness.org/activity v1.17.0
|
||||
# code.superseriousbusiness.org/activity v1.18.0
|
||||
## explicit; go 1.23
|
||||
code.superseriousbusiness.org/activity/pub
|
||||
code.superseriousbusiness.org/activity/streams
|
||||
|
||||
Reference in New Issue
Block a user