Skip to content

Commit a99f7e0

Browse files
dahliacodex
andcommitted
Stop retries for gone actors
Handle Fedify 2.1.0 unverified Delete activities whose signing key fetch now returns 410 Gone by acknowledging the request with 202 Accepted. This prevents remote servers from repeatedly redelivering deletes for actors that are already permanently gone. Keep these unverified activities out of the normal BotKit event handler flow, add regression coverage for the decision logic, and document the behavior in the changelog and events guide. fedify-dev/fedify#472 https://fedify.dev/manual/inbox Co-Authored-By: OpenAI Codex <codex@openai.com>
1 parent 4c84b39 commit a99f7e0

5 files changed

Lines changed: 141 additions & 16 deletions

File tree

CHANGES.md

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,27 @@ To be released.
88

99
### @fedify/botkit
1010

11+
- Upgraded Fedify to 2.1.0.
12+
13+
- BotKit now targets Fedify 2.0's modular package layout, using
14+
*@fedify/vocab*, *@fedify/vocab-runtime*, and *@fedify/denokv*
15+
where appropriate.
16+
- `Message.language` and `SessionPublishOptions.language` now use
17+
`Intl.Locale` instead of `LanguageTag`.
18+
- Bot software versions now use plain strings instead of `SemVer`
19+
objects.
20+
- Removed the `parseSemVer()`, `SemVer`, `LanguageTag`, and
21+
`parseLanguageTag()` public exports.
22+
23+
- BotKit now acknowledges unverified remote `Delete` activities signed by
24+
permanently gone actors with `202 Accepted` instead of `401 Unauthorized`.
25+
26+
- This applies only when Fedify reports a `keyFetchError` and the
27+
remote actor's key fetch returned `410 Gone`.
28+
- The unverified activity is not passed to BotKit event handlers, but
29+
the successful response stops repeated redelivery attempts from the
30+
remote server.
31+
1132
- Added FEP-5711 inverse properties to the bot actor's `outbox` and
1233
`followers` collections.
1334

@@ -23,18 +44,6 @@ To be released.
2344
automatically redirects to the appropriate follow page using the OStatus
2445
subscribe protocol.
2546

26-
- Upgraded Fedify to 2.0.3.
27-
28-
- BotKit now targets Fedify 2.0's modular package layout, using
29-
*@fedify/vocab*, *@fedify/vocab-runtime*, and *@fedify/denokv*
30-
where appropriate.
31-
- `Message.language` and `SessionPublishOptions.language` now use
32-
`Intl.Locale` instead of `LanguageTag`.
33-
- Bot software versions now use plain strings instead of `SemVer`
34-
objects.
35-
- Removed the `parseSemVer()`, `SemVer`, `LanguageTag`, and
36-
`parseLanguageTag()` public exports.
37-
3847
[#10]: https://github.com/fedify-dev/botkit/issues/10
3948
[#14]: https://github.com/fedify-dev/botkit/pull/14
4049

deno.lock

Lines changed: 23 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/concepts/events.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ bot.onMention = async (session, message) => {
2525
Every event handler receives a [session](./session.md) object as the first
2626
argument, and the event-specific object as the second argument.
2727

28+
BotKit invokes these event handlers only for activities whose signatures were
29+
verified by Fedify. As of Fedify 2.1.0, BotKit also acknowledges certain
30+
unverified remote `Delete` activities from actors whose keys now return
31+
`410 Gone`, but those activities are not dispatched to `on*` handlers.
32+
2833
The following is a list of events that BotKit supports:
2934

3035

packages/botkit/src/bot-impl.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
//
1414
// You should have received a copy of the GNU Affero General Public License
1515
// along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
import type { UnverifiedActivityReason } from "@fedify/fedify";
1617
import { type InboxContext, MemoryKvStore } from "@fedify/fedify/federation";
1718
import {
1819
Accept,
@@ -22,6 +23,7 @@ import {
2223
Article,
2324
Collection,
2425
Create,
26+
Delete,
2527
Emoji,
2628
EmojiReact,
2729
Follow,
@@ -1205,6 +1207,78 @@ test("BotImpl.dispatchSharedKey()", () => {
12051207
assert.deepStrictEqual(bot.dispatchSharedKey(ctx), { identifier });
12061208
});
12071209

1210+
test("BotImpl.onUnverifiedActivity()", async (t) => {
1211+
const bot = new BotImpl<void>({
1212+
kv: new MemoryKvStore(),
1213+
username: "bot",
1214+
});
1215+
const ctx = bot.federation.createContext(
1216+
new Request("https://example.com/ap/inbox", { method: "POST" }),
1217+
undefined,
1218+
);
1219+
const deleteActivity = new Delete({
1220+
id: new URL("https://remote.example/activities/delete"),
1221+
actor: new URL("https://remote.example/actors/deleted"),
1222+
object: new URL("https://remote.example/actors/deleted"),
1223+
});
1224+
const createActivity = new Create({
1225+
id: new URL("https://remote.example/activities/create"),
1226+
actor: new URL("https://remote.example/actors/deleted"),
1227+
object: new Note({
1228+
id: new URL("https://remote.example/notes/1"),
1229+
attribution: new URL("https://remote.example/actors/deleted"),
1230+
content: "Hello, world!",
1231+
}),
1232+
});
1233+
const keyFetch410: UnverifiedActivityReason = {
1234+
type: "keyFetchError",
1235+
keyId: new URL("https://remote.example/actors/deleted#main-key"),
1236+
result: {
1237+
status: 410,
1238+
response: new Response(null, { status: 410 }),
1239+
},
1240+
};
1241+
1242+
await t.test("acknowledges gone actor deletes", () => {
1243+
const response = bot.onUnverifiedActivity(ctx, deleteActivity, keyFetch410);
1244+
assert.ok(response instanceof Response);
1245+
assert.deepStrictEqual(response.status, 202);
1246+
});
1247+
1248+
await t.test("ignores non-410 key fetch failures", () => {
1249+
const reason: UnverifiedActivityReason = {
1250+
...keyFetch410,
1251+
result: {
1252+
status: 404,
1253+
response: new Response(null, { status: 404 }),
1254+
},
1255+
};
1256+
const response = bot.onUnverifiedActivity(ctx, deleteActivity, reason);
1257+
assert.deepStrictEqual(response, undefined);
1258+
});
1259+
1260+
await t.test("ignores non-delete activities", () => {
1261+
const response = bot.onUnverifiedActivity(ctx, createActivity, keyFetch410);
1262+
assert.deepStrictEqual(response, undefined);
1263+
});
1264+
1265+
await t.test("ignores other verification failures", () => {
1266+
const noSignature: UnverifiedActivityReason = { type: "noSignature" };
1267+
const invalidSignature: UnverifiedActivityReason = {
1268+
type: "invalidSignature",
1269+
keyId: new URL("https://remote.example/actors/deleted#main-key"),
1270+
};
1271+
assert.deepStrictEqual(
1272+
bot.onUnverifiedActivity(ctx, deleteActivity, noSignature),
1273+
undefined,
1274+
);
1275+
assert.deepStrictEqual(
1276+
bot.onUnverifiedActivity(ctx, deleteActivity, invalidSignature),
1277+
undefined,
1278+
);
1279+
});
1280+
});
1281+
12081282
for (const policy of ["accept", "reject", "manual"] as const) {
12091283
test(`BotImpl.onFollowed() [followerPolicy: ${policy}]`, async (t) => {
12101284
const repository = new MemoryRepository();

packages/botkit/src/bot-impl.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
type PageItems,
2424
type RequestContext,
2525
type Software,
26+
type UnverifiedActivityReason,
2627
} from "@fedify/fedify";
2728
import {
2829
Accept,
@@ -33,6 +34,7 @@ import {
3334
Article,
3435
ChatMessage,
3536
Create,
37+
Delete,
3638
Emoji as APEmoji,
3739
EmojiReact,
3840
Endpoints,
@@ -236,6 +238,7 @@ export class BotImpl<TContextData> implements Bot<TContextData> {
236238
);
237239
this.federation
238240
.setInboxListeners("/ap/actor/{identifier}/inbox", "/ap/inbox")
241+
.onUnverifiedActivity(this.onUnverifiedActivity.bind(this))
239242
.on(Follow, this.onFollowed.bind(this))
240243
.on(Undo, async (ctx, undo) => {
241244
const object = await undo.getObject(ctx);
@@ -552,6 +555,21 @@ export class BotImpl<TContextData> implements Bot<TContextData> {
552555
return { identifier: this.identifier };
553556
}
554557

558+
onUnverifiedActivity(
559+
_ctx: RequestContext<TContextData>,
560+
activity: Activity,
561+
reason: UnverifiedActivityReason,
562+
): Response | void {
563+
if (
564+
activity instanceof Delete &&
565+
reason.type === "keyFetchError" &&
566+
"status" in reason.result &&
567+
reason.result.status === 410
568+
) {
569+
return new Response(null, { status: 202 });
570+
}
571+
}
572+
555573
async onFollowed(
556574
ctx: InboxContext<TContextData>,
557575
follow: Follow,

0 commit comments

Comments
 (0)