[feature] Support new model of interaction flow for forward compat with v0.21.0 (#4394)

~~Still WIP!~~

This PR allows v0.20.0 of GtS to be forward-compatible with the interaction request / authorization flow that will fully replace the current flow in v0.21.0.

Basically, this means we need to recognize LikeRequest, ReplyRequest, and AnnounceRequest, and in response to those requests, deliver either a Reject or an Accept, with the latter pointing towards a LikeAuthorization, ReplyAuthorization, or AnnounceAuthorization, respectively. This can then be used by the remote instance to prove to third parties that the interaction has been accepted by the interactee. These Authorization types need to be dereferencable to third parties, so we need to serve them.

As well as recognizing the above "polite" interaction request types, we also need to still serve appropriate responses to "impolite" interaction request types, where an instance that's unaware of interaction policies tries to interact with a post by sending a reply, like, or boost directly, without wrapping it in a WhateverRequest type.

Doesn't fully close https://codeberg.org/superseriousbusiness/gotosocial/issues/4026 but gets damn near (just gotta update the federating with GtS documentation).

Migrations tested on both Postgres and SQLite.

Co-authored-by: kim <grufwub@gmail.com>
Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4394
Co-authored-by: tobi <tobi.smethurst@protonmail.com>
Co-committed-by: tobi <tobi.smethurst@protonmail.com>
This commit is contained in:
tobi 2025-09-14 15:37:35 +02:00 committed by tobi
commit 754b7be9cf
126 changed files with 6637 additions and 1778 deletions

View file

@ -23,15 +23,18 @@ import (
"code.superseriousbusiness.org/activity/pub"
)
// PublicURI returns a fresh copy of the *url.URL version of the
// magic ActivityPub URI https://www.w3.org/ns/activitystreams#Public
func PublicURI() *url.URL {
publicURI, err := url.Parse(pub.PublicActivityPubIRI)
// publicIRI is a pre-parsed global public IRI instance.
var publicIRI = func() *url.URL {
url, err := url.Parse(pub.PublicActivityPubIRI)
if err != nil {
panic(err)
}
return publicURI
}
return url
}()
// PublicIRI returns a fresh copy of the *url.URL version of the
// magic ActivityPub URI https://www.w3.org/ns/activitystreams#Public
func PublicIRI() *url.URL { var u url.URL; u = *publicIRI; return &u }
// https://www.w3.org/TR/activitystreams-vocabulary
const (
@ -102,9 +105,12 @@ const (
/* GtS stuff */
ObjectLikeApproval = "LikeApproval"
ObjectReplyApproval = "ReplyApproval"
ObjectAnnounceApproval = "AnnounceApproval"
ActivityLikeRequest = "LikeRequest"
ActivityReplyRequest = "ReplyRequest"
ActivityAnnounceRequest = "AnnounceRequest"
ObjectLikeAuthorization = "LikeAuthorization"
ObjectReplyAuthorization = "ReplyAuthorization"
ObjectAnnounceAuthorization = "AnnounceAuthorization"
/* Funkwhale stuff */
@ -138,7 +144,10 @@ func isActivity(typeName string) bool {
ActivityAnnounce,
ActivityBlock,
ActivityFlag,
ActivityDislike:
ActivityDislike,
ActivityLikeRequest,
ActivityReplyRequest,
ActivityAnnounceRequest:
return true
default:
return false

View file

@ -110,7 +110,7 @@ func noteWithMentions1() vocab.ActivityStreamsNote {
// Anyone can like.
canLikeAlwaysProp := streams.NewGoToSocialAlwaysProperty()
canLikeAlwaysProp.AppendIRI(ap.PublicURI())
canLikeAlwaysProp.AppendIRI(ap.PublicIRI())
canLike.SetGoToSocialAlways(canLikeAlwaysProp)
// Empty approvalRequired.
@ -127,7 +127,7 @@ func noteWithMentions1() vocab.ActivityStreamsNote {
// Anyone can reply.
canReplyAlwaysProp := streams.NewGoToSocialAlwaysProperty()
canReplyAlwaysProp.AppendIRI(ap.PublicURI())
canReplyAlwaysProp.AppendIRI(ap.PublicIRI())
canReply.SetGoToSocialAlways(canReplyAlwaysProp)
// Set empty approvalRequired.
@ -150,7 +150,7 @@ func noteWithMentions1() vocab.ActivityStreamsNote {
// Public requires approval to announce.
canAnnounceApprovalRequiredProp := streams.NewGoToSocialApprovalRequiredProperty()
canAnnounceApprovalRequiredProp.AppendIRI(ap.PublicURI())
canAnnounceApprovalRequiredProp.AppendIRI(ap.PublicIRI())
canAnnounce.SetGoToSocialApprovalRequired(canAnnounceApprovalRequiredProp)
// Set canAnnounce on the policy.
@ -265,7 +265,7 @@ func addressable1() ap.Addressable {
note := streams.NewActivityStreamsNote()
toProp := streams.NewActivityStreamsToProperty()
toProp.AppendIRI(ap.PublicURI())
toProp.AppendIRI(ap.PublicIRI())
note.SetActivityStreamsTo(toProp)
@ -287,7 +287,7 @@ func addressable2() ap.Addressable {
note.SetActivityStreamsTo(toProp)
ccProp := streams.NewActivityStreamsCcProperty()
ccProp.AppendIRI(ap.PublicURI())
ccProp.AppendIRI(ap.PublicIRI())
note.SetActivityStreamsCc(ccProp)

View file

@ -36,7 +36,7 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/util"
)
// ExtractObjects will extract object vocab.Types from given implementing interface.
// ExtractObjects will extract object TypeOrIRIs from given implementing interface.
func ExtractObjects(with WithObject) []TypeOrIRI {
// Extract the attached object (if any).
objProp := with.GetActivityStreamsObject()
@ -58,6 +58,28 @@ func ExtractObjects(with WithObject) []TypeOrIRI {
return objs
}
// ExtractInstrument will extract instrument TypeOrIRIs from given implementing interface.
func ExtractInstruments(with WithInstrument) []TypeOrIRI {
// Extract the attached instrument (if any).
instrProp := with.GetActivityStreamsInstrument()
if instrProp == nil {
return nil
}
// Check for invalid len.
if instrProp.Len() == 0 {
return nil
}
// Accumulate all of the instruments into a slice.
instrs := make([]TypeOrIRI, instrProp.Len())
for i := range instrProp.Len() {
instrs[i] = instrProp.At(i)
}
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) {
switch typeName := activity.GetTypeName(); {
@ -1222,14 +1244,14 @@ func ExtractInteractionPolicy(
statusable Statusable,
owner *gtsmodel.Account,
) *gtsmodel.InteractionPolicy {
ipa, ok := statusable.(InteractionPolicyAware)
wip, ok := statusable.(WithInteractionPolicy)
if !ok {
// Not a type with interaction
// policy properties settable.
return nil
}
policyProp := ipa.GetGoToSocialInteractionPolicy()
policyProp := wip.GetGoToSocialInteractionPolicy()
if policyProp == nil || policyProp.Len() != 1 {
return nil
}

View file

@ -143,13 +143,13 @@ func ToAcceptable(t vocab.Type) (Acceptable, bool) {
return acceptable, true
}
// IsApprovable returns whether AS vocab type name
// is something that can be cast to Approvable.
func IsApprovable(typeName string) bool {
// IsAuthorizationable returns whether AS vocab type name
// is something that can be cast to Authorizationable.
func IsAuthorizationable(typeName string) bool {
switch typeName {
case ObjectLikeApproval,
ObjectReplyApproval,
ObjectAnnounceApproval:
case ObjectLikeAuthorization,
ObjectReplyAuthorization,
ObjectAnnounceAuthorization:
return true
default:
return false
@ -157,12 +157,12 @@ func IsApprovable(typeName string) bool {
}
// ToAcceptable safely tries to cast vocab.Type as Approvable.
func ToApprovable(t vocab.Type) (Approvable, bool) {
approvable, ok := t.(Approvable)
if !ok || !IsApprovable(t.GetTypeName()) {
func ToAuthorizationable(t vocab.Type) (Authorizationable, bool) {
authable, ok := t.(Authorizationable)
if !ok || !IsAuthorizationable(t.GetTypeName()) {
return nil, false
}
return approvable, true
return authable, true
}
// IsAttachmentable returns whether AS vocab type name
@ -188,6 +188,36 @@ func ToAttachmentable(t vocab.Type) (Attachmentable, bool) {
return attachmentable, true
}
// IsAnnounceable returns whether AS vocab type name
// is something that can be cast to vocab.ActivityStreamsAnnounce.
func IsAnnounceable(typeName string) bool {
return typeName == ActivityAnnounce
}
// ToAnnounceable safely tries to cast vocab.Type as vocab.ActivityStreamsAnnounce.
func ToAnnounceable(t vocab.Type) (vocab.ActivityStreamsAnnounce, bool) {
announceable, ok := t.(vocab.ActivityStreamsAnnounce)
if !ok || t.GetTypeName() != ActivityAnnounce {
return nil, false
}
return announceable, true
}
// IsLikeable returns whether AS vocab type name
// is something that can be cast to vocab.ActivityStreamsLike.
func IsLikeable(typeName string) bool {
return typeName == ActivityLike
}
// ToAnnouncToLikeableeable safely tries to cast vocab.Type as vocab.ActivityStreamsLike.
func ToLikeable(t vocab.Type) (vocab.ActivityStreamsLike, bool) {
likeable, ok := t.(vocab.ActivityStreamsLike)
if !ok || t.GetTypeName() != ActivityLike {
return nil, false
}
return likeable, true
}
// Activityable represents the minimum activitypub interface for representing an 'activity'.
// (see: IsActivityable() for types implementing this, though you MUST make sure to check
// the typeName as this bare interface may be implementable by non-Activityable types).
@ -258,11 +288,6 @@ type Statusable interface {
WithReplies
}
type InteractionPolicyAware interface {
WithInteractionPolicy
WithApprovedBy
}
// Pollable represents the minimum activitypub interface for representing a 'poll' (it's a subset of a status).
// (see: IsPollable() for types implementing this, though you MUST make sure to check
// the typeName as this bare interface may be implementable by non-Pollable types).
@ -299,14 +324,14 @@ type Acceptable interface {
WithResult
}
// Approvable represents the minimum activitypub interface
// for a LikeApproval, ReplyApproval, or AnnounceApproval.
type Approvable interface {
// Authorizationable represents the minimum interface for a
// LikeAuthorization, ReplyAuthorization, AnnounceAuthorization.
type Authorizationable interface {
vocab.Type
WithAttributedTo
WithObject
WithTarget
WithInteractingObject
WithInteractionTarget
}
// Attachmentable represents the minimum activitypub interface for representing a 'mediaAttachment'. (see: IsAttachmentable).
@ -392,6 +417,16 @@ type ReplyToable interface {
WithInReplyTo
}
// InteractionRequestable represents the minimum interface for an interaction request
// activity, eg., LikeRequest, ReplyRequest, AnnounceRequest, QuoteRequest, etc..
type InteractionRequestable interface {
vocab.Type
WithActor
WithObject
WithInstrument
}
// CollectionIterator represents the minimum interface for interacting with a
// wrapped Collection or OrderedCollection in order to access next / prev items.
type CollectionIterator interface {
@ -683,6 +718,12 @@ type WithObject interface {
SetActivityStreamsObject(vocab.ActivityStreamsObjectProperty)
}
// WithInstrument represents an activity with ActivityStreamsInstrumentProperty
type WithInstrument interface {
GetActivityStreamsInstrument() vocab.ActivityStreamsInstrumentProperty
SetActivityStreamsInstrument(vocab.ActivityStreamsInstrumentProperty)
}
// WithTarget represents an activity with ActivityStreamsTargetProperty
type WithTarget interface {
GetActivityStreamsTarget() vocab.ActivityStreamsTargetProperty
@ -775,14 +816,44 @@ type WithPolicyRules interface {
GetGoToSocialApprovalRequired() vocab.GoToSocialApprovalRequiredProperty // Deprecated
}
// WithApprovedBy represents a Statusable with the approvedBy property.
// WithApprovedBy represents an object with the approvedBy property.
type WithApprovedBy interface {
GetGoToSocialApprovedBy() vocab.GoToSocialApprovedByProperty
SetGoToSocialApprovedBy(vocab.GoToSocialApprovedByProperty)
}
// WithVotersCount represents an activity or object the result property.
// WithLikeAuthorization represents a Likeable with the likeAuthorization property.
type WithLikeAuthorization interface {
GetGoToSocialLikeAuthorization() vocab.GoToSocialLikeAuthorizationProperty
SetGoToSocialLikeAuthorization(vocab.GoToSocialLikeAuthorizationProperty)
}
// WithReplyAuthorization represents a statusable with the replyAuthorization property.
type WithReplyAuthorization interface {
GetGoToSocialReplyAuthorization() vocab.GoToSocialReplyAuthorizationProperty
SetGoToSocialReplyAuthorization(vocab.GoToSocialReplyAuthorizationProperty)
}
// WithAnnounceAuthorization represents an Announceable with the announceAuthorization property.
type WithAnnounceAuthorization interface {
GetGoToSocialAnnounceAuthorization() vocab.GoToSocialAnnounceAuthorizationProperty
SetGoToSocialAnnounceAuthorization(vocab.GoToSocialAnnounceAuthorizationProperty)
}
// WithResult represents an activity or object with the result property.
type WithResult interface {
GetActivityStreamsResult() vocab.ActivityStreamsResultProperty
SetActivityStreamsResult(vocab.ActivityStreamsResultProperty)
}
// WithInteractingObject represents an activity or object with the InteractingObject property.
type WithInteractingObject interface {
GetGoToSocialInteractingObject() vocab.GoToSocialInteractingObjectProperty
SetGoToSocialInteractingObject(vocab.GoToSocialInteractingObjectProperty)
}
// WithInteractionTarget represents an activity or object with the InteractionTarget property.
type WithInteractionTarget interface {
GetGoToSocialInteractionTarget() vocab.GoToSocialInteractionTargetProperty
SetGoToSocialInteractionTarget(vocab.GoToSocialInteractionTargetProperty)
}

View file

@ -226,6 +226,36 @@ func AppendObjectIRIs(with WithObject, object ...*url.URL) {
}, object...)
}
// AppendInstrumentIRIs appends the given IRIs to the Instrument property of 'with'.
func AppendInstrumentIRIs(with WithInstrument, instrument ...*url.URL) {
appendIRIs(func() Property[vocab.ActivityStreamsInstrumentPropertyIterator] {
instrumentProp := with.GetActivityStreamsInstrument()
if instrumentProp == nil {
instrumentProp = streams.NewActivityStreamsInstrumentProperty()
with.SetActivityStreamsInstrument(instrumentProp)
}
return instrumentProp
}, instrument...)
}
// GetResultIRIs returns the IRIs contained in the `result` property of 'with'.
func GetResultIRIs(with WithResult) []*url.URL {
resultProp := with.GetActivityStreamsResult()
return extractIRIs(resultProp)
}
// AppendResultIRIs appends the given IRIs to the Result property of 'with'.
func AppendResultIRIs(with WithResult, result ...*url.URL) {
appendIRIs(func() Property[vocab.ActivityStreamsResultPropertyIterator] {
resultProp := with.GetActivityStreamsResult()
if resultProp == nil {
resultProp = streams.NewActivityStreamsResultProperty()
with.SetActivityStreamsResult(resultProp)
}
return resultProp
}, result...)
}
// GetTargetIRIs returns the IRIs contained in the Target property of 'with'.
func GetTargetIRIs(with WithTarget) []*url.URL {
targetProp := with.GetActivityStreamsTarget()
@ -262,6 +292,42 @@ func AppendAttributedTo(with WithAttributedTo, attribTo ...*url.URL) {
}, attribTo...)
}
// GetInteractingObject returns IRIs contained in the interactingObject property of 'with'.
func GetInteractingObject(with WithInteractingObject) []*url.URL {
intObjProp := with.GetGoToSocialInteractingObject()
return getIRIs(intObjProp)
}
// AppendInteractingObject appends the given IRIs to the interactingObject property of 'with'.
func AppendInteractingObject(with WithInteractingObject, interactingObject ...*url.URL) {
appendIRIs(func() Property[vocab.GoToSocialInteractingObjectPropertyIterator] {
intObjProp := with.GetGoToSocialInteractingObject()
if intObjProp == nil {
intObjProp = streams.NewGoToSocialInteractingObjectProperty()
with.SetGoToSocialInteractingObject(intObjProp)
}
return intObjProp
}, interactingObject...)
}
// GetInteractionTarget returns IRIs contained in the interactionTarget property of 'with'.
func GetInteractionTarget(with WithInteractionTarget) []*url.URL {
intTargetProp := with.GetGoToSocialInteractionTarget()
return getIRIs(intTargetProp)
}
// AppendInteractionTarget appends the given IRIs to the interactionTarget property of 'with'.
func AppendInteractionTarget(with WithInteractionTarget, interactionTarget ...*url.URL) {
appendIRIs(func() Property[vocab.GoToSocialInteractionTargetPropertyIterator] {
intTargetProp := with.GetGoToSocialInteractionTarget()
if intTargetProp == nil {
intTargetProp = streams.NewGoToSocialInteractionTargetProperty()
with.SetGoToSocialInteractionTarget(intTargetProp)
}
return intTargetProp
}, interactionTarget...)
}
// GetInReplyTo returns the IRIs contained in the InReplyTo property of 'with'.
func GetInReplyTo(with WithInReplyTo) []*url.URL {
replyProp := with.GetActivityStreamsInReplyTo()
@ -607,11 +673,11 @@ func SetHidesCcPublicFromUnauthedWeb(with WithHidesCcPublicFromUnauthedWeb, hide
// GetApprovedBy returns the URL contained in
// the ApprovedBy property of 'with', if set.
func GetApprovedBy(with WithApprovedBy) *url.URL {
mafProp := with.GetGoToSocialApprovedBy()
if mafProp == nil || !mafProp.IsIRI() {
abProp := with.GetGoToSocialApprovedBy()
if abProp == nil || !abProp.IsIRI() {
return nil
}
return mafProp.Get()
return abProp.Get()
}
// SetApprovedBy sets the given url
@ -625,6 +691,69 @@ func SetApprovedBy(with WithApprovedBy, approvedBy *url.URL) {
abProp.Set(approvedBy)
}
// GetLikeAuthorization returns the URL contained in
// the likeAuthorization property of 'with', if set.
func GetLikeAuthorization(with WithLikeAuthorization) *url.URL {
laProp := with.GetGoToSocialLikeAuthorization()
if laProp == nil || !laProp.IsIRI() {
return nil
}
return laProp.Get()
}
// SetLikeAuthorization sets the given url on
// the 'likeAuthorization' property of 'with'.
func SetLikeAuthorization(with WithLikeAuthorization, likeAuthorization *url.URL) {
laProp := with.GetGoToSocialLikeAuthorization()
if laProp == nil {
laProp = streams.NewGoToSocialLikeAuthorizationProperty()
with.SetGoToSocialLikeAuthorization(laProp)
}
laProp.Set(likeAuthorization)
}
// GetReplyAuthorization returns the URL contained in
// the replyAuthorization property of 'with', if set.
func GetReplyAuthorization(with WithReplyAuthorization) *url.URL {
raProp := with.GetGoToSocialReplyAuthorization()
if raProp == nil || !raProp.IsIRI() {
return nil
}
return raProp.Get()
}
// SetReplyAuthorization sets the given url on
// the 'replyAuthorization' property of 'with'.
func SetReplyAuthorization(with WithReplyAuthorization, replyAuthorization *url.URL) {
raProp := with.GetGoToSocialReplyAuthorization()
if raProp == nil {
raProp = streams.NewGoToSocialReplyAuthorizationProperty()
with.SetGoToSocialReplyAuthorization(raProp)
}
raProp.Set(replyAuthorization)
}
// GetAnnounceAuthorization returns the URL contained in
// the announceAuthorization property of 'with', if set.
func GetAnnounceAuthorization(with WithAnnounceAuthorization) *url.URL {
aaProp := with.GetGoToSocialAnnounceAuthorization()
if aaProp == nil || !aaProp.IsIRI() {
return nil
}
return aaProp.Get()
}
// SetAnnounceAuthorization sets the given url on
// the 'announceAuthorization' property of 'with'.
func SetAnnounceAuthorization(with WithAnnounceAuthorization, announceAuthorization *url.URL) {
aaProp := with.GetGoToSocialAnnounceAuthorization()
if aaProp == nil {
aaProp = streams.NewGoToSocialAnnounceAuthorizationProperty()
with.SetGoToSocialAnnounceAuthorization(aaProp)
}
aaProp.Set(announceAuthorization)
}
// GetMediaType returns the string contained in
// the MediaType property of 'with', if set.
func GetMediaType(with WithMediaType) string {
@ -689,6 +818,50 @@ func SetBlurhash(with WithBlurhash, mediaType string) {
bProp.Set(mediaType)
}
// AppendSensitive appends the given sensitive
// boolean to the `sensitive` property of 'with'.
func AppendSensitive(with WithSensitive, sensitive bool) {
sProp := with.GetActivityStreamsSensitive()
if sProp == nil {
sProp = streams.NewActivityStreamsSensitiveProperty()
with.SetActivityStreamsSensitive(sProp)
}
sProp.AppendXMLSchemaBoolean(sensitive)
}
// AppendContent appends the given content
// string to the `content` property of 'with'.
func AppendContent(with WithContent, content string) {
cProp := with.GetActivityStreamsContent()
if cProp == nil {
cProp = streams.NewActivityStreamsContentProperty()
with.SetActivityStreamsContent(cProp)
}
cProp.AppendXMLSchemaString(content)
}
// AppendContentMap appends the given content
// language map to the `content` property of 'with'.
func AppendContentMap(with WithContent, contentMap map[string]string) {
cProp := with.GetActivityStreamsContent()
if cProp == nil {
cProp = streams.NewActivityStreamsContentProperty()
with.SetActivityStreamsContent(cProp)
}
cProp.AppendRDFLangString(contentMap)
}
// SetReplies sets the given replies collection
// to the `replies` property of 'with'.
func SetReplies(with WithReplies, replies vocab.ActivityStreamsCollection) {
rProp := with.GetActivityStreamsReplies()
if rProp == nil {
rProp = streams.NewActivityStreamsRepliesProperty()
with.SetActivityStreamsReplies(rProp)
}
rProp.SetActivityStreamsCollection(replies)
}
// extractIRIs extracts just the AP IRIs from an iterable
// property that may contain types (with IRIs) or just IRIs.
//

View file

@ -153,8 +153,8 @@ func serializeStatusable(t vocab.Type, includeContext bool) (map[string]interfac
NormalizeOutgoingAttachmentProp(statusable, data)
NormalizeOutgoingContentProp(statusable, data)
if ipa, ok := statusable.(InteractionPolicyAware); ok {
NormalizeOutgoingInteractionPolicyProp(ipa, data)
if wip, ok := statusable.(WithInteractionPolicy); ok {
NormalizeOutgoingInteractionPolicyProp(wip, data)
}
return data, nil