Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ federation
context.getActorUri(IDENTIFIER),
),
actor: follow.objectId,
object: follow,
object: follow.id,
}),
);
relationStore.set(follower.id.href, follower);
Expand Down
4 changes: 2 additions & 2 deletions docs/manual/context-advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,15 +91,15 @@ import { Accept, Follow } from "@fedify/vocab";
federation
.setInboxListeners("/users/{identifier}/inbox", "/inbox")
.on(Follow, async (ctx, follow) => {
if (follow.objectId == null) return;
if (follow.id == null || follow.objectId == null) return;
const parsed = ctx.parseUri(follow.objectId); // [!code highlight]
if (parsed?.type !== "actor") return; // [!code highlight]
const recipient = await follow.getActor(ctx);
if (recipient == null) return;
await ctx.sendActivity(
{ identifier: parsed.identifier },
recipient,
new Accept({ actor: follow.objectId, object: follow }),
new Accept({ actor: follow.objectId, object: follow.id }),
);
});
~~~~
Expand Down
4 changes: 2 additions & 2 deletions docs/manual/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,15 +153,15 @@ federation
.setInboxListeners("/users/{identifier}/inbox", "/inbox")
.on(Follow, async (ctx, follow) => {
// In order to send an activity, we need the identifier of the sender actor:
if (follow.objectId == null) return;
if (follow.id == null || follow.objectId == null) return;
const parsed = ctx.parseUri(follow.objectId);
if (parsed?.type !== "actor") return;
const recipient = await follow.getActor(ctx);
if (recipient == null) return;
await ctx.sendActivity(
{ identifier: parsed.identifier }, // sender
recipient,
new Accept({ actor: follow.objectId, object: follow }),
new Accept({ actor: follow.objectId, object: follow.id }),
);
});
~~~~
Expand Down
4 changes: 2 additions & 2 deletions docs/manual/inbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,15 +158,15 @@ const federation = createFederation({
federation
.setInboxListeners("/users/{identifier}/inbox", "/inbox")
.on(Follow, async (ctx, follow) => {
if (follow.objectId == null) return;
if (follow.id == null || follow.objectId == null) return;
const parsed = ctx.parseUri(follow.objectId);
if (parsed?.type !== "actor") return;
const recipient = await follow.getActor(ctx);
if (recipient == null) return;
await ctx.sendActivity(
{ identifier: parsed.identifier },
recipient,
new Accept({ actor: follow.objectId, object: follow }),
new Accept({ actor: follow.objectId, object: follow.id }),
);
});
~~~~
Expand Down
23 changes: 13 additions & 10 deletions docs/manual/migrate.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,15 +303,15 @@ const federation = null as unknown as Federation<void>;
federation
.setInboxListeners("/u/{identifier}/inbox", "/inbox")
.on(Follow, async (ctx, follow) => {
if (follow.objectId == null) return;
if (follow.id == null || follow.objectId == null) return;
const parsed = ctx.parseUri(follow.objectId);
if (parsed?.type !== "actor") return;
const follower = await follow.getActor(ctx);
if (follower == null) return;
await ctx.sendActivity(
{ identifier: parsed.identifier },
follower,
new Accept({ actor: follow.objectId, object: follow }),
new Accept({ actor: follow.objectId, object: follow.id }),
);
});
~~~~
Expand Down Expand Up @@ -600,6 +600,7 @@ federation
federation
.setInboxListeners("/u/{identifier}/inbox", "/inbox")
.on(Follow, async (ctx, follow) => {
if (follow.id == null) return;
const parsed = follow.objectId == null
? null
: ctx.parseUri(follow.objectId);
Expand All @@ -609,7 +610,7 @@ federation
await ctx.sendActivity(
{ identifier: parsed.identifier },
follower,
new Accept({ actor: follow.objectId!, object: follow }),
new Accept({ actor: follow.objectId!, object: follow.id }),
);
});

Expand Down Expand Up @@ -859,15 +860,15 @@ const federation = null as unknown as Federation<void>;
federation
.setInboxListeners("/u/{identifier}/inbox", "/inbox")
.on(Follow, async (ctx, follow) => {
if (follow.objectId == null) return;
if (follow.id == null || follow.objectId == null) return;
const parsed = ctx.parseUri(follow.objectId);
if (parsed?.type !== "actor") return;
const follower = await follow.getActor(ctx);
if (follower == null) return;
await ctx.sendActivity(
{ identifier: parsed.identifier },
follower,
new Accept({ actor: follow.objectId, object: follow }),
new Accept({ actor: follow.objectId, object: follow.id }),
);
});
~~~~
Expand Down Expand Up @@ -1113,6 +1114,7 @@ federation
federation
.setInboxListeners("/u/{identifier}/inbox", "/inbox")
.on(Follow, async (ctx, follow) => {
if (follow.id == null) return;
const parsed = follow.objectId == null
? null
: ctx.parseUri(follow.objectId);
Expand All @@ -1122,7 +1124,7 @@ federation
await ctx.sendActivity(
{ identifier: parsed.identifier },
follower,
new Accept({ actor: follow.objectId!, object: follow }),
new Accept({ actor: follow.objectId!, object: follow.id }),
);
});

Expand Down Expand Up @@ -1325,15 +1327,15 @@ async function removeFollower(_: {
federation
.setInboxListeners("/u/{identifier}/inbox", "/inbox")
.on(Follow, async (ctx, follow) => {
if (follow.objectId == null) return;
if (follow.id == null || follow.objectId == null) return;
const parsed = ctx.parseUri(follow.objectId);
if (parsed?.type !== "actor") return;
const follower = await follow.getActor(ctx);
if (follower == null) return;
await ctx.sendActivity(
{ identifier: parsed.identifier },
follower,
new Accept({ actor: follow.objectId, object: follow }),
new Accept({ actor: follow.objectId, object: follow.id }),
);
})
.on(Undo, async (ctx, undo) => {
Expand Down Expand Up @@ -1409,7 +1411,7 @@ await ctx.sendActivity(
follower,
new Accept({
actor: ctx.getActorUri(identifier),
object: follow,
object: follow.id,
}),
);
~~~~
Expand Down Expand Up @@ -1660,6 +1662,7 @@ federation
federation
.setInboxListeners("/u/{identifier}/inbox", "/inbox")
.on(Follow, async (ctx, follow) => {
if (follow.id == null) return;
const parsed = follow.objectId == null
? null
: ctx.parseUri(follow.objectId);
Expand All @@ -1672,7 +1675,7 @@ federation
await ctx.sendActivity(
{ identifier: parsed.identifier },
follower,
new Accept({ actor: follow.objectId!, object: follow }),
new Accept({ actor: follow.objectId!, object: follow.id }),
);
})
.on(Undo, async (ctx, undo) => {
Expand Down
2 changes: 1 addition & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"markdown-it-abbr": "^2.0.0",
"markdown-it-deflist": "^3.0.0",
"markdown-it-footnote": "^4.0.0",
"markdown-it-jsr-ref": "0.4.4",
"markdown-it-jsr-ref": "0.5.0",
"mermaid": "^11.4.1",
"postgres": "catalog:",
"srvx": "^0.11.15",
Expand Down
8 changes: 4 additions & 4 deletions docs/tutorial/astro-blog.md
Original file line number Diff line number Diff line change
Expand Up @@ -1295,7 +1295,7 @@ federation
ctx.getActorUri(BLOG_IDENTIFIER),
),
actor: ctx.getActorUri(BLOG_IDENTIFIER),
object: follow,
object: follow.id,
}),
);
})
Expand Down Expand Up @@ -1740,7 +1740,7 @@ federation
ctx.getActorUri(BLOG_IDENTIFIER),
),
actor: ctx.getActorUri(BLOG_IDENTIFIER),
object: follow,
object: follow.id,
}),
);
})
Expand Down Expand Up @@ -2047,7 +2047,7 @@ federation
ctx.getActorUri(BLOG_IDENTIFIER),
),
actor: ctx.getActorUri(BLOG_IDENTIFIER),
object: follow,
object: follow.id,
}),
);
})
Expand Down Expand Up @@ -2603,7 +2603,7 @@ federation
ctx.getActorUri(BLOG_IDENTIFIER),
),
actor: ctx.getActorUri(BLOG_IDENTIFIER),
object: follow,
object: follow.id,
}),
);
})
Expand Down
9 changes: 7 additions & 2 deletions docs/tutorial/basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -1125,7 +1125,7 @@ federation
await ctx.sendActivity(
{ identifier: parsed.identifier },
follower,
new Accept({ actor: follow.objectId, object: follow }),
new Accept({ actor: follow.objectId, object: follow.id }),
);
});
~~~~
Expand All @@ -1134,6 +1134,11 @@ Restart the server, and make a follow request from your Mastodon account to
the actor *me*. You should see the server immediately accept the follow
request.

The `Accept` refers to the original `Follow` by its ID instead of embedding
the whole activity. The `Follow` belongs to the remote server's origin, so a
copy embedded in our `Accept` would not be authoritative under Fedify's
[origin-based security model](../manual/vocab.md#origin-based-security-model).

> [!TIP]
> If you are curious about sending activities further, see the [*Sending
> activities* section](../manual/send.md) in the manual.
Expand Down Expand Up @@ -1165,7 +1170,7 @@ federation
await ctx.sendActivity(
{ identifier: parsed.identifier },
follower,
new Accept({ actor: follow.objectId, object: follow }),
new Accept({ actor: follow.objectId, object: follow.id }),
);
// Store the follower in the key–value store:
await kv.set(["followers", follow.id.href], follow.actorId.href);
Expand Down
36 changes: 17 additions & 19 deletions docs/tutorial/content-sharing.md
Original file line number Diff line number Diff line change
Expand Up @@ -4588,7 +4588,7 @@ import {
InProcessMessageQueue,
MemoryKvStore,
} from "@fedify/fedify";
import { Accept, Follow } from "@fedify/vocab";
import { Accept } from "@fedify/vocab";
import { eq, or } from "drizzle-orm";
import { db } from "./db/client";
import { following } from "./db/schema";
Expand All @@ -4601,37 +4601,35 @@ export const federation = createFederation<void>({
federation
.setInboxListeners("/users/{identifier}/inbox", "/inbox")
// ---cut---
.on(Accept, async (_ctx, accept) => {
.on(Accept, async (ctx, accept) => {
// The remote server has accepted alice's outbound Follow.
// Match the Accept against our `following` row by either the
// original Follow's `id` (when the peer echoes it) or the
// remote actor URI (Pixelfed sometimes mints a fresh id on the
// way back), and flip the row's status to "accepted".
if (accept.actorId == null) return;
const followObject = await accept.getObject();
const followActivityId =
followObject instanceof Follow ? followObject.id?.href : null;
if (accept.actorId == null || accept.objectId == null) return;
const followActivityId = accept.objectId.href;
const remoteActorUri = accept.actorId.href;
const matcher = followActivityId
? or(
eq(following.followActivityId, followActivityId),
eq(following.actorUri, remoteActorUri),
)
: eq(following.actorUri, remoteActorUri);
const matcher = accept.objectId.origin === ctx.canonicalOrigin
? eq(following.followActivityId, followActivityId)
: or(
eq(following.followActivityId, followActivityId),
eq(following.actorUri, remoteActorUri),
);
await db
.update(following)
.set({ status: "accepted" })
.where(matcher);
});
~~~~

`accept.getObject()` resolves the embedded Follow.
Mastodon and GoToSocial both keep the original Follow's `id`,
which makes matching unambiguous. Pixelfed, in contrast, often
returns a freshly-minted Follow with a different `id`, so even
when an `id` is present it may not be the one we sent. Combining
the two predicates with `or` lets the row flip to *accepted*
whichever path the peer took.
`accept.objectId` reads the referenced activity's ID without trusting or
dereferencing an embedded `Follow` from another origin. Mastodon and
GoToSocial keep the original `Follow` ID, which makes matching unambiguous.
Pixelfed often returns a freshly minted ID instead, so the remote actor URI
remains the interoperability fallback in this single-user application. The
handler only uses that fallback for an ID minted on another origin; a mismatched
ID on our own origin is rejected instead of accepting an unrelated activity.

> [!TIP]
> If you ship multiple local users later (the closing chapter
Expand Down
Loading
Loading