Skip to content

Commit 19ee65e

Browse files
committed
Republish bot profile updates
Add Session.republishProfile() so bots can broadcast an ActivityPub Update for their current actor profile to followers. The staged changes also add regression coverage for the new session API, update the session and bot concept docs, and record the feature in CHANGES.md. Closes #18
1 parent a99f7e0 commit 19ee65e

7 files changed

Lines changed: 118 additions & 0 deletions

File tree

CHANGES.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,17 @@ To be released.
4444
automatically redirects to the appropriate follow page using the OStatus
4545
subscribe protocol.
4646

47+
- Added `Session.republishProfile()` to broadcast profile changes to
48+
followers. [[#18]]
49+
50+
- The new method sends an ActivityPub `Update` activity for the bot
51+
actor to the bot's followers.
52+
- This makes profile updates such as display name, bio, avatar, and
53+
header image propagate without waiting for the next post.
54+
4755
[#10]: https://github.com/fedify-dev/botkit/issues/10
4856
[#14]: https://github.com/fedify-dev/botkit/pull/14
57+
[#18]: https://github.com/fedify-dev/botkit/issues/18
4958

5059

5160
Version 0.3.1

docs/concepts/bot.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ The type of the bot actor. It can be either `Application` or `Service`.
126126
The display name of the bot. It can be changed after the bot is
127127
federated.
128128

129+
If you change it after deployment and want followers to notice the change
130+
immediately, call
131+
[`Session.republishProfile()`](./session.md#republishing-the-bot-profile).
132+
129133
### `~CreateBotOptions.summary`
130134

131135
The description of the bot. It will be displayed in the bio field
@@ -134,6 +138,10 @@ of the profile. It can be changed after the bot is federated.
134138
Note that it does not take a string, but a `Text` object.
135139
See also the [*Text* chapter](./text.md).
136140

141+
If you change it after deployment and want followers to notice the change
142+
immediately, call
143+
[`Session.republishProfile()`](./session.md#republishing-the-bot-profile).
144+
137145
### `~CreateBotOptions.icon`
138146

139147
The avatar URL of the bot. It can be changed after the bot is federated.
@@ -144,6 +152,10 @@ The avatar URL of the bot. It can be changed after the bot is federated.
144152
> the avatar image so that it looks fine on the most fediverse platforms,
145153
> you need to change the image file itself.
146154
155+
If you change it after deployment and want followers to notice the change
156+
immediately, call
157+
[`Session.republishProfile()`](./session.md#republishing-the-bot-profile).
158+
147159
### `~CreateBotOptions.image`
148160

149161
The header image URL of the bot. It can be changed after the bot is federated.
@@ -154,6 +166,10 @@ The header image URL of the bot. It can be changed after the bot is federated.
154166
> the header image so that it looks fine on the most fediverse platforms,
155167
> you need to change the image file itself.
156168
169+
If you change it after deployment and want followers to notice the change
170+
immediately, call
171+
[`Session.republishProfile()`](./session.md#republishing-the-bot-profile).
172+
157173
### `~CreateBotOptions.properties`
158174

159175
The custom properties of the bot. Usually you would like to put some metadata

docs/concepts/session.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,26 @@ const actor: Actor = await session.getActor();
131131
~~~~
132132

133133

134+
Republishing the bot profile
135+
----------------------------
136+
137+
If you change the bot's profile metadata, such as the display name, bio,
138+
avatar, or header image, remote servers may keep showing the old cached
139+
profile until they refresh it themselves. You can explicitly notify your
140+
followers by calling the `~Session.republishProfile()` method:
141+
142+
~~~~ typescript twoslash
143+
import type { Session } from "@fedify/botkit";
144+
const session = {} as unknown as Session<void>;
145+
// ---cut-before---
146+
await session.republishProfile();
147+
~~~~
148+
149+
This sends an ActivityPub `Update` activity for the bot actor to the bot's
150+
followers. Call it after your application updates the bot profile and you want
151+
the change to propagate without waiting for the next post.
152+
153+
134154
Publishing a message
135155
--------------------
136156

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
Question,
2525
type Recipient,
2626
Undo,
27+
Update,
2728
} from "@fedify/vocab";
2829
import assert from "node:assert";
2930
import { describe, test } from "node:test";
@@ -264,6 +265,43 @@ describe("SessionImpl.follows()", () => {
264265
});
265266
});
266267

268+
test("SessionImpl.republishProfile()", async () => {
269+
const bot = new BotImpl<void>({
270+
kv: new MemoryKvStore(),
271+
username: "bot",
272+
name: "Bot display name",
273+
summary: text`This is the bot profile.`,
274+
icon: new URL("https://example.com/icon.png"),
275+
image: new URL("https://example.com/header.png"),
276+
});
277+
const ctx = createMockContext(bot, "https://example.com");
278+
const session = new SessionImpl(bot, ctx);
279+
280+
await session.republishProfile();
281+
282+
assert.deepStrictEqual(ctx.sentActivities.length, 1);
283+
const { recipients, activity } = ctx.sentActivities[0];
284+
assert.deepStrictEqual(recipients, "followers");
285+
assert.ok(activity instanceof Update);
286+
assert.deepStrictEqual(activity.actorId, ctx.getActorUri(bot.identifier));
287+
assert.deepStrictEqual(activity.toIds, [ctx.getFollowersUri(bot.identifier)]);
288+
assert.deepStrictEqual(activity.ccIds, []);
289+
const actor = await activity.getObject(ctx);
290+
assert.ok(actor instanceof bot.class);
291+
assert.deepStrictEqual(actor.id, session.actorId);
292+
assert.deepStrictEqual(actor.name?.toString(), "Bot display name");
293+
assert.deepStrictEqual(
294+
actor.summary?.toString(),
295+
"<p>This is the bot profile.</p>",
296+
);
297+
const icon = await actor.getIcon();
298+
assert.ok(icon != null);
299+
assert.deepStrictEqual(icon.url, new URL("https://example.com/icon.png"));
300+
const image = await actor.getImage();
301+
assert.ok(image != null);
302+
assert.deepStrictEqual(image.url, new URL("https://example.com/header.png"));
303+
});
304+
267305
test("SessionImpl.publish()", async (t) => {
268306
const kv = new MemoryKvStore();
269307
const bot = new BotImpl<void>({ kv, username: "bot" });

packages/botkit/src/session-impl.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
type Object,
2828
PUBLIC_COLLECTION,
2929
Undo,
30+
Update,
3031
} from "@fedify/vocab";
3132
import { getLogger } from "@logtape/logtape";
3233
import { encode } from "html-entities";
@@ -213,6 +214,25 @@ export class SessionImpl<TContextData> implements Session<TContextData> {
213214
return follow != null;
214215
}
215216

217+
async republishProfile(): Promise<void> {
218+
const actor = await this.getActor();
219+
const update = new Update({
220+
id: new URL(`#update-profile/${crypto.randomUUID()}`, this.actorId),
221+
actor: this.actorId,
222+
to: this.context.getFollowersUri(this.bot.identifier),
223+
object: actor,
224+
});
225+
await this.context.sendActivity(
226+
this.bot,
227+
"followers",
228+
update,
229+
{
230+
preferSharedInbox: true,
231+
excludeBaseUris: [new URL(this.context.origin)],
232+
},
233+
);
234+
}
235+
216236
async publish(
217237
content: Text<"block", TContextData>,
218238
options?: SessionImplPublishOptions<TContextData>,

packages/botkit/src/session.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,18 @@ export interface Session<TContextData> {
104104
*/
105105
follows(actor: Actor | URL | string): Promise<boolean>;
106106

107+
/**
108+
* Republishes the bot profile to its followers.
109+
*
110+
* This is useful when the bot's profile metadata such as its display name,
111+
* bio, avatar, or header image has changed and remote servers need to be
112+
* notified explicitly.
113+
*
114+
* @returns A promise that resolves when the profile update activity is sent.
115+
* @since 0.4.0
116+
*/
117+
republishProfile(): Promise<void>;
118+
107119
/**
108120
* Publishes a message attributed to the bot.
109121
* @param text The content of the note.

packages/botkit/src/text.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@ const bot: BotWithVoidContextData = {
145145
publish() {
146146
throw new Error("Not implemented");
147147
},
148+
republishProfile() {
149+
throw new Error("Not implemented");
150+
},
148151
getOutbox() {
149152
throw new Error("Not implemented");
150153
},

0 commit comments

Comments
 (0)