Skip to content

Commit d80ed60

Browse files
committed
AuthorizedMessage.update() method
1 parent efc3411 commit d80ed60

File tree

6 files changed

+336
-25
lines changed

6 files changed

+336
-25
lines changed

docs/concepts/message.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,32 @@ session.getOutbox({
460460
[`AsyncIterable`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols
461461

462462

463+
Updating a message
464+
------------------
465+
466+
You can update a message's content by calling the `~AuthorizedMessage.update()`
467+
method:
468+
469+
~~~~ typescript
470+
const message = await session.publish(
471+
text`This message will be updated in a minute.`
472+
);
473+
setTimeout(async () => {
474+
await message.update(text`This message has been updated.`); // [!code highlight]
475+
}, 1000 * 60);
476+
~~~~
477+
478+
> [!NOTE]
479+
> Since the `~AuthorizedMessage.update()` method belongs to
480+
> the `AuthorizedMessage` type, you cannot call it on an unauthorized `Message`
481+
> object.
482+
483+
> [!CAUTION]
484+
> Some ActivityPub implementations like older versions of Mastodon and Misskey
485+
> do not support updating messages. For those implementations, once published
486+
> messages are shown as-is forever even if you update them.
487+
488+
463489
Deleting a message
464490
------------------
465491

@@ -474,6 +500,11 @@ setTimeout(async () => {
474500
}, 1000 * 60);
475501
~~~~
476502

503+
> [!NOTE]
504+
> Since the `~AuthorizedMessage.delete()` method belongs to
505+
> the `AuthorizedMessage` type, you cannot call it on an unauthorized `Message`
506+
> object.
507+
477508

478509
Replying to a message
479510
---------------------

src/bot-impl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,7 @@ export class BotImpl<TContextData> implements Bot<TContextData> {
692692
let messageCache: Message<MessageClass, TContextData> | null = null;
693693
const getMessage = async () => {
694694
if (messageCache != null) return messageCache;
695-
return messageCache = await createMessage(object, session);
695+
return messageCache = await createMessage(object, session, {});
696696
};
697697
const replyTarget = ctx.parseUri(object.replyTargetId);
698698
if (

src/message-impl.test.ts

Lines changed: 147 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
PUBLIC_COLLECTION,
2626
Tombstone,
2727
Undo,
28+
Update,
2829
} from "@fedify/fedify/vocab";
2930
import { assert } from "@std/assert/assert";
3031
import { assertEquals } from "@std/assert/equals";
@@ -40,7 +41,7 @@ Deno.test("createMessage()", async () => {
4041
const bot = new BotImpl<void>({ kv: new MemoryKvStore(), username: "bot" });
4142
const session = bot.getSession("https://example.com", undefined);
4243
await assertRejects(
43-
() => createMessage<Note, void>(new Note({}), session),
44+
() => createMessage<Note, void>(new Note({}), session, {}),
4445
TypeError,
4546
"The raw.id is required.",
4647
);
@@ -49,6 +50,7 @@ Deno.test("createMessage()", async () => {
4950
createMessage<Note, void>(
5051
new Note({ id: new URL("https://example.com/notes/1") }),
5152
session,
53+
{},
5254
),
5355
TypeError,
5456
"The raw.content is required.",
@@ -61,6 +63,7 @@ Deno.test("createMessage()", async () => {
6163
content: "<p>Hello, world!</p>",
6264
}),
6365
session,
66+
{},
6467
),
6568
TypeError,
6669
"The raw.attributionId is required.",
@@ -83,7 +86,11 @@ Deno.test("createMessage()", async () => {
8386
}),
8487
],
8588
});
86-
const publicMessage = await createMessage<Note, void>(publicNote, session);
89+
const publicMessage = await createMessage<Note, void>(
90+
publicNote,
91+
session,
92+
{},
93+
);
8794
assertEquals(publicMessage.raw, publicNote);
8895
assertEquals(publicMessage.id, publicNote.id);
8996
assertEquals(publicMessage.actor, await session.getActor());
@@ -110,6 +117,7 @@ Deno.test("createMessage()", async () => {
110117
const unlistedMessage = await createMessage<Note, void>(
111118
unlistedNote,
112119
session,
120+
{},
113121
);
114122
assertEquals(unlistedMessage.visibility, "unlisted");
115123

@@ -120,21 +128,22 @@ Deno.test("createMessage()", async () => {
120128
const followersMessage = await createMessage<Note, void>(
121129
followersNote,
122130
session,
131+
{},
123132
);
124133
assertEquals(followersMessage.visibility, "followers");
125134

126135
const direct = publicNote.clone({
127136
to: new URL("https://example.com/ap/actor/bot"),
128137
ccs: [],
129138
});
130-
const directMessage = await createMessage<Note, void>(direct, session);
139+
const directMessage = await createMessage<Note, void>(direct, session, {});
131140
assertEquals(directMessage.visibility, "direct");
132141

133142
const unknown = publicNote.clone({
134143
tos: [],
135144
ccs: [],
136145
});
137-
const unknownMessage = await createMessage<Note, void>(unknown, session);
146+
const unknownMessage = await createMessage<Note, void>(unknown, session, {});
138147
assertEquals(unknownMessage.visibility, "unknown");
139148
});
140149

@@ -152,7 +161,13 @@ Deno.test("AuthorizedMessageImpl.delete()", async () => {
152161
to: PUBLIC_COLLECTION,
153162
cc: new URL("https://example.com/ap/actor/bot/followers"),
154163
});
155-
const msg = await createMessage<Note, void>(note, session, undefined, true);
164+
const msg = await createMessage<Note, void>(
165+
note,
166+
session,
167+
{},
168+
undefined,
169+
true,
170+
);
156171
await kv.set(
157172
bot.kvPrefixes.messages,
158173
["c1c792ce-a0be-4685-b396-e59e5ef8c788"],
@@ -208,7 +223,11 @@ Deno.test("MessageImpl.reply()", async () => {
208223
to: new URL("https://example.com/ap/actor/john/followers"),
209224
cc: PUBLIC_COLLECTION,
210225
});
211-
const originalMsg = await createMessage<Note, void>(originalPost, session);
226+
const originalMsg = await createMessage<Note, void>(
227+
originalPost,
228+
session,
229+
{},
230+
);
212231
const reply = await originalMsg.reply(text`Hello, John!`);
213232
const msgIds = await kv.get<string[]>(bot.kvPrefixes.messages);
214233
assert(msgIds != null);
@@ -251,7 +270,11 @@ Deno.test("MessageImpl.share()", async (t) => {
251270
to: new URL("https://example.com/ap/actor/john/followers"),
252271
cc: PUBLIC_COLLECTION,
253272
});
254-
const originalMsg = await createMessage<Note, void>(originalPost, session);
273+
const originalMsg = await createMessage<Note, void>(
274+
originalPost,
275+
session,
276+
{},
277+
);
255278
const sharedMsg = await originalMsg.share();
256279
let msgId: string;
257280

@@ -307,3 +330,120 @@ Deno.test("MessageImpl.share()", async (t) => {
307330
assertEquals(activity.objectId, sharedMsg.id);
308331
});
309332
});
333+
334+
Deno.test("AuthorizedMessage.update()", async (t) => {
335+
const kv = new MemoryKvStore();
336+
const bot = new BotImpl<void>({ kv, username: "bot" });
337+
const ctx = createMockContext(bot, "https://example.com");
338+
const session = new SessionImpl(bot, ctx);
339+
const actorA = new Person({
340+
id: new URL("https://example.com/ap/actor/john"),
341+
preferredUsername: "john",
342+
});
343+
const actorB = new Person({
344+
id: new URL("https://example.com/ap/actor/jane"),
345+
preferredUsername: "jane",
346+
});
347+
348+
for (
349+
const visibility of ["public", "unlisted", "followers", "direct"] as const
350+
) {
351+
await t.step(visibility, async () => {
352+
const msg = await session.publish(text`Hello, ${actorA}`, { visibility });
353+
const [id] = await kv.get<string[]>(bot.kvPrefixes.messages) ?? [];
354+
const originalRaw = msg.raw;
355+
ctx.sentActivities = [];
356+
const before = Temporal.Now.instant();
357+
await msg.update(text`Hello, ${actorB}`);
358+
const after = Temporal.Now.instant();
359+
assertEquals(msg.text, "Hello, @jane@example.com");
360+
assertEquals(
361+
msg.html,
362+
'<p>Hello, <a href="https://example.com/ap/actor/jane" ' +
363+
'translate="no" class="h-card u-url mention" target="_blank">' +
364+
"@<span>jane@example.com</span></a></p>",
365+
);
366+
assertEquals(msg.mentions.length, 1);
367+
assertEquals(msg.mentions[0].id, actorB.id);
368+
assertEquals(msg.hashtags, []);
369+
assert(msg.updated != null);
370+
assert(msg.updated.epochNanoseconds >= before.epochNanoseconds);
371+
assert(msg.updated.epochNanoseconds <= after.epochNanoseconds);
372+
assertEquals(msg.raw.content, msg.html);
373+
if (visibility === "public") {
374+
assertEquals(msg.raw.toIds, [PUBLIC_COLLECTION, actorB.id]);
375+
assertEquals(msg.raw.ccIds, [ctx.getFollowersUri(bot.identifier)]);
376+
} else if (visibility === "unlisted") {
377+
assertEquals(msg.raw.toIds, [
378+
ctx.getFollowersUri(bot.identifier),
379+
actorB.id,
380+
]);
381+
assertEquals(msg.raw.ccIds, [PUBLIC_COLLECTION]);
382+
} else if (visibility === "followers") {
383+
assertEquals(msg.raw.toIds, [
384+
ctx.getFollowersUri(bot.identifier),
385+
actorB.id,
386+
]);
387+
assertEquals(msg.raw.ccIds, []);
388+
} else {
389+
assertEquals(msg.raw.toIds, [actorB.id]);
390+
assertEquals(msg.raw.ccIds, []);
391+
}
392+
const tags = await Array.fromAsync(msg.raw.getTags());
393+
assertEquals(tags.length, 1);
394+
assertInstanceOf(tags[0], Mention);
395+
assertEquals(tags[0].name, "@jane@example.com");
396+
assertEquals(tags[0].href, actorB.id);
397+
assertEquals(msg.raw.published, originalRaw.published);
398+
assertEquals(msg.raw.updated, msg.updated);
399+
const createJson = await kv.get([...bot.kvPrefixes.messages, id]);
400+
assert(createJson != null);
401+
const create = await Create.fromJsonLd(createJson);
402+
assertEquals(
403+
await (await create.getObject())?.toJsonLd({ format: "compact" }),
404+
await msg.raw.toJsonLd({ format: "compact" }),
405+
);
406+
assertEquals(ctx.sentActivities.length, visibility === "direct" ? 1 : 2);
407+
const { recipients, activity } = ctx.sentActivities[0];
408+
assertEquals(
409+
recipients,
410+
visibility === "direct" ? [actorA, actorB] : "followers",
411+
);
412+
assertInstanceOf(activity, Update);
413+
assertEquals(activity.actorId, ctx.getActorUri(bot.identifier));
414+
if (visibility === "public") {
415+
assertEquals(activity.toIds, [PUBLIC_COLLECTION, actorA.id, actorB.id]);
416+
assertEquals(activity.ccIds, [ctx.getFollowersUri(bot.identifier)]);
417+
} else if (visibility === "unlisted") {
418+
assertEquals(activity.toIds, [
419+
ctx.getFollowersUri(bot.identifier),
420+
actorA.id,
421+
actorB.id,
422+
]);
423+
assertEquals(activity.ccIds, [PUBLIC_COLLECTION]);
424+
} else if (visibility === "followers") {
425+
assertEquals(activity.toIds, [
426+
ctx.getFollowersUri(bot.identifier),
427+
actorA.id,
428+
actorB.id,
429+
]);
430+
assertEquals(activity.ccIds, []);
431+
} else {
432+
assertEquals(activity.toIds, [actorA.id, actorB.id]);
433+
assertEquals(activity.ccIds, []);
434+
}
435+
assertEquals(await activity.getObject(), msg.raw);
436+
assertEquals(activity.updated, msg.updated);
437+
if (visibility !== "direct") {
438+
const { recipients, activity } = ctx.sentActivities[1];
439+
assertEquals(recipients, [actorA, actorB]);
440+
assertInstanceOf(activity, Update);
441+
assertEquals(activity.actorId, ctx.getActorUri(bot.identifier));
442+
assertEquals(await activity.getObject(), msg.raw);
443+
assertEquals(activity.updated, msg.updated);
444+
}
445+
});
446+
447+
await kv.delete(bot.kvPrefixes.messages);
448+
}
449+
});

0 commit comments

Comments
 (0)