Interaction controls
Interaction controls let an object owner publish policy and exchange explicit request and authorization objects before another actor interacts with that object. They are useful when an application wants automatic or manual approval for interactions such as likes, replies, announces, quotes, and featuring an actor in a public collection.
Fedify provides the @fedify/interaction-controls package for the helper logic around the vocabulary terms defined by GoToSocial interaction controls, FEP-044f, and FEP-7aa9. The package does not install inbox listeners for you. It gives you typed helpers that you can call from your own outbox, inbox, storage, moderation, and UI code.
Supported interactions
The package exports one helper per interaction:
likeInteraction- Works with
LikeRequest,LikeAuthorization, andLike. It evaluatescanLike. replyInteraction- Works with
ReplyRequest,ReplyAuthorization, and reply objects such asNote,Article,Question, andChatMessage. It evaluatescanReply. announceInteraction- Works with
AnnounceRequest,AnnounceAuthorization, andAnnounce. It evaluatescanAnnounce. quoteInteraction- Works with
QuoteRequest,QuoteAuthorization, and quote objects. It evaluatescanQuoteand accepts both the FEP-044fquoteproperty and compatiblequoteUrlvalues. featureInteraction- Works with
FeatureRequest,FeatureAuthorization,FeaturedCollection, and ActivityPub actor objects. It evaluatescanFeatureon the actor being featured.
Policy evaluation
Each helper can evaluate an object's InteractionPolicy and report whether the interaction should be accepted automatically, queued for manual approval, or denied:
const target = new Note({
id: new URL("https://example.com/notes/1"),
attribution: new URL("https://example.com/users/alice"),
interactionPolicy: new InteractionPolicy({
canLike: new InteractionRule({
automaticApproval: PUBLIC_COLLECTION,
}),
}),
});
const decision = await likeInteraction.evaluatePolicy(context, {
subject: target,
requester: new URL("https://remote.example/users/bob"),
});A decision with result: "automatic" means the interaction can be accepted without a moderation step. A decision with result: "manual" means the application should store the request for review and create the authorization only after approval. A decision with result: "denied" means the request does not match the policy.
Missing policy is handled conservatively for feature and quote requests, and permissively for legacy-compatible interactions:
- Like, reply, and announce helpers treat missing policy as automatic approval for compatibility with existing ActivityPub objects.
- The quote helper treats missing
canQuotepolicy as denied, because FEP-044f quote authorization is consent-based unless the object owner explicitly advertises automatic approval. - The feature helper treats missing
canFeaturepolicy as denied, because featuring another actor is a profile/discovery action.
End-to-end flow
The helpers cover the request, authorization, and final interaction checks. Your application still owns delivery, persistence, and moderation:
Request flow
When your actor wants to perform an interaction, create a request activity and send it to the object owner. The request actor is the actor asking for permission, the object is the interaction target, and the instrument is the object or collection that would perform the interaction:
import { likeInteraction } from "@fedify/interaction-controls";
import { Like, Note } from "@fedify/vocab";
const actor = new URL("https://remote.example/users/bob");
const target = new Note({
id: new URL("https://example.com/notes/1"),
attribution: new URL("https://example.com/users/alice"),
});
const like = new Like({
id: new URL("https://remote.example/likes/1"),
actor,
object: target.id,
});
const request = likeInteraction.createRequest({
id: new URL("https://remote.example/requests/1"),
actor,
object: target,
instrument: like,
});On the receiving side, verify that the request is dereferenceable, has the expected type, and that the instrument matches both the requester and target:
const verified = await likeInteraction.verifyRequest(context, { request });
if (!verified.verified) {
throw new Error(`Invalid interaction request: ${verified.failure.type}`);
}For replyInteraction, quoteInteraction, and featureInteraction, the request instrument has this meaning:
- Reply: the reply object whose
inReplyTotarget is being requested. - Quote: the post object whose FEP-044f
quoteor compatiblequoteUrltarget is being requested. - Feature: the
FeaturedCollectionowned by the requester. The requestobjectis the actor being featured.
Authorization flow
After a policy decision is automatic or a moderator approves a manual request, create an authorization object and include it with the resulting interaction:
import { likeInteraction } from "@fedify/interaction-controls";
import { Like, Note } from "@fedify/vocab";
const owner = new URL("https://example.com/users/alice");
const target = new Note({
id: new URL("https://example.com/notes/1"),
attribution: owner,
});
const like = new Like({
id: new URL("https://remote.example/likes/1"),
actor: new URL("https://remote.example/users/bob"),
object: target.id,
});
const authorization = likeInteraction.createAuthorization({
id: new URL("https://example.com/authorizations/1"),
attributedTo: owner,
interactingObject: like,
interactionTarget: target,
});When a signed interaction arrives with an authorization, verify that the authorization still refers to the same interaction object and target, and that the grant came from the target owner. If you pass an embedded authorization object instead of a URL, provide verifyAuthenticity so your HTTP signature, object proof, or transport-level trust decision is part of verification:
const verified = await likeInteraction.verifyAuthorization(context, {
authorization,
interactingObject: like,
interactionTarget: target,
attributedTo: target.attributionId ?? undefined,
verifyAuthenticity: verifyEmbeddedAuthorization,
});
if (!verified.verified) {
throw new Error(`Invalid authorization: ${verified.failure.type}`);
}You can also create Accept, Reject, and revocation activities from the same helper. Store authorization IDs with the interaction object so that later revocation checks can reject stale approvals.
Recognizing unrequested interactions
Some remote servers will send a bare Like, reply, announce, or quote without first sending a request. The helpers expose recognizeImpolite() for best-effort detection:
import { likeInteraction } from "@fedify/interaction-controls";
import { Like } from "@fedify/vocab";
const activity = new Like({
id: new URL("https://remote.example/likes/1"),
actor: new URL("https://remote.example/users/bob"),
object: new URL("https://example.com/notes/1"),
});
const recognized = likeInteraction.recognizeImpolite(activity);This method is synchronous and only recognizes objects whose interaction target can be read without dereferencing. In particular, Create activities wrapping reply or quote objects are not recognized by this method; unwrap and verify the created object in your inbox listener before passing it to the helper.
Stable storage keys
Each helper provides stable keys for persistence:
import {
formatAuthorizationKey,
formatInteractionKey,
likeInteraction,
} from "@fedify/interaction-controls";
import { Like, LikeAuthorization, Note } from "@fedify/vocab";
const actor = new URL("https://remote.example/users/bob");
const target = new URL("https://example.com/notes/1");
const like = new Like({
id: new URL("https://remote.example/likes/1"),
actor,
object: target,
});
const authorization = new URL("https://example.com/authorizations/1");
const interactionKey = formatInteractionKey(likeInteraction.getInteractionKey({
requester: actor,
interactingObject: like,
interactionTarget: new Note({ id: target }),
}));
const authorizationKey = formatAuthorizationKey(
likeInteraction.getAuthorizationKey({
authorization: new LikeAuthorization({ id: authorization }),
}),
);The keys are plain strings. Use them with your existing database or key–value store to record pending requests, accepted authorizations, and revoked authorizations.