Skip to content

Commit 4591cbc

Browse files
committed
Bot.onLike event
1 parent 94b4841 commit 4591cbc

File tree

6 files changed

+184
-9
lines changed

6 files changed

+184
-9
lines changed

docs/concepts/events.md

+21
Original file line numberDiff line numberDiff line change
@@ -274,3 +274,24 @@ bot.onSharedMessage = async (session, sharedMessage) => {
274274
> [!TIP]
275275
> The `~Bot.onSharedMessage` event handler can be called when someone your
276276
> bot does not follow shares a message on your bot.
277+
278+
279+
Like
280+
----
281+
282+
The `~Bot.onLike` event handler is called when someone likes messages on your
283+
bot or actors your bot follows. It receives a `Like` object, which represents
284+
the like activity, as the second argument.
285+
286+
The following is an example of a like event handler that sends a direct message
287+
when someone likes a message on your bot:
288+
289+
~~~~ typescript
290+
bot.onLike = async (session, like) => {
291+
if (like.message.actor.id?.href !== session.actorId.href) return;
292+
await session.publish(
293+
text`Thanks for liking my message, ${like.actor}!`,
294+
{ visibility: "direct" },
295+
);
296+
};
297+
~~~~

src/bot-impl.test.ts

+34
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
CryptographicKey,
2525
Follow,
2626
Image,
27+
Like as RawLike,
2728
Mention,
2829
Note,
2930
Person,
@@ -43,6 +44,7 @@ import { BotImpl } from "./bot-impl.ts";
4344
import { parseSemVer } from "./bot.ts";
4445
import type { FollowRequest } from "./follow.ts";
4546
import type { Message, MessageClass, SharedMessage } from "./message.ts";
47+
import type { Like } from "./reaction.ts";
4648
import { MemoryRepository } from "./repository.ts";
4749
import { SessionImpl } from "./session-impl.ts";
4850
import type { Session } from "./session.ts";
@@ -1821,6 +1823,38 @@ Deno.test("BotImpl.onAnnounced()", async () => {
18211823
assertEquals(ctx.forwardedRecipients, []);
18221824
});
18231825

1826+
Deno.test("BotImpl.onLiked()", async () => {
1827+
const bot = new BotImpl<void>({
1828+
kv: new MemoryKvStore(),
1829+
username: "bot",
1830+
});
1831+
const likes: [Session<void>, Like<void>][] = [];
1832+
bot.onLike = (session, like) => void (likes.push([session, like]));
1833+
const ctx = createMockInboxContext(bot, "https://example.com", "bot");
1834+
const rawLike = new RawLike({
1835+
id: new URL("https://example.com/ap/actor/bot/announce/1"),
1836+
actor: new URL("https://example.com/ap/actor/bot"),
1837+
object: new Note({
1838+
id: new URL("https://example.com/ap/actor/bot/note/1"),
1839+
attribution: new URL("https://example.com/ap/actor/bot"),
1840+
to: PUBLIC_COLLECTION,
1841+
cc: new URL("https://example.com/ap/actor/bot/followers"),
1842+
content: "Hello, world!",
1843+
}),
1844+
});
1845+
await bot.onLiked(ctx, rawLike);
1846+
assertEquals(likes.length, 1);
1847+
const [session, like] = likes[0];
1848+
assertEquals(session.bot, bot);
1849+
assertEquals(session.context, ctx);
1850+
assertEquals(like.raw, rawLike);
1851+
assertEquals(like.id, rawLike.id);
1852+
assertEquals(like.actor.id, rawLike.actorId);
1853+
assertEquals(like.message.id, rawLike.objectId);
1854+
assertEquals(ctx.sentActivities, []);
1855+
assertEquals(ctx.forwardedRecipients, []);
1856+
});
1857+
18241858
Deno.test("BotImpl.dispatchNodeInfo()", () => {
18251859
const bot = new BotImpl<void>({
18261860
kv: new MemoryKvStore(),

src/bot-impl.ts

+39-6
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
Image,
3232
type InboxContext,
3333
isActor,
34+
Like as RawLike,
3435
type Link,
3536
Mention,
3637
type NodeInfo,
@@ -56,6 +57,7 @@ import type { Bot, CreateBotOptions, PagesOptions } from "./bot.ts";
5657
import type {
5758
AcceptEventHandler,
5859
FollowEventHandler,
60+
LikeEventHandler,
5961
MentionEventHandler,
6062
MessageEventHandler,
6163
RejectEventHandler,
@@ -67,10 +69,12 @@ import { FollowRequestImpl } from "./follow-impl.ts";
6769
import {
6870
createMessage,
6971
getMessageVisibility,
72+
isMessageObject,
7073
messageClasses,
7174
} from "./message-impl.ts";
7275
import type { Message, MessageClass, SharedMessage } from "./message.ts";
7376
import { app } from "./pages.tsx";
77+
import type { Like } from "./reaction.ts";
7478
import { KvRepository, type Repository, type Uuid } from "./repository.ts";
7579
import { SessionImpl } from "./session-impl.ts";
7680
import type { Session } from "./session.ts";
@@ -108,6 +112,7 @@ export class BotImpl<TContextData> implements Bot<TContextData> {
108112
onReply?: ReplyEventHandler<TContextData>;
109113
onMessage?: MessageEventHandler<TContextData>;
110114
onSharedMessage?: SharedMessageEventHandler<TContextData>;
115+
onLike?: LikeEventHandler<TContextData>;
111116

112117
constructor(options: BotImplOptions<TContextData>) {
113118
this.identifier = options.identifier ?? "bot";
@@ -207,6 +212,7 @@ export class BotImpl<TContextData> implements Bot<TContextData> {
207212
.on(Reject, this.onFollowRejected.bind(this))
208213
.on(Create, this.onCreated.bind(this))
209214
.on(Announce, this.onAnnounced.bind(this))
215+
.on(RawLike, this.onLiked.bind(this))
210216
.setSharedKeyDispatcher(this.dispatchSharedKey.bind(this));
211217
if (this.software != null) {
212218
this.federation.setNodeInfoDispatcher(
@@ -654,12 +660,7 @@ export class BotImpl<TContextData> implements Bot<TContextData> {
654660
} else {
655661
object = await announce.getObject(ctx);
656662
}
657-
if (
658-
!(object instanceof Article || object instanceof ChatMessage ||
659-
object instanceof Note || object instanceof Question)
660-
) {
661-
return;
662-
}
663+
if (!isMessageObject(object)) return;
663664
const session = this.getSession(ctx);
664665
const actor = announce.actorId.href == session.actorId.href
665666
? await session.getActor()
@@ -676,6 +677,38 @@ export class BotImpl<TContextData> implements Bot<TContextData> {
676677
await this.onSharedMessage(session, sharedMessage);
677678
}
678679

680+
async onLiked(ctx: InboxContext<TContextData>, like: RawLike): Promise<void> {
681+
if (this.onLike == null || like.id == null || like.actorId == null) return;
682+
const objectUri = ctx.parseUri(like.objectId);
683+
let object: Object | null = null;
684+
if (
685+
objectUri?.type === "object" &&
686+
// deno-lint-ignore no-explicit-any
687+
messageClasses.includes(objectUri.class as any)
688+
) {
689+
const msg = await this.repository.getMessage(objectUri.values.id as Uuid);
690+
if (msg instanceof Create) object = await msg.getObject(ctx);
691+
} else {
692+
object = await like.getObject(ctx);
693+
}
694+
if (!isMessageObject(object)) return;
695+
const session = this.getSession(ctx);
696+
const actor = like.actorId.href == session.actorId.href
697+
? await session.getActor()
698+
: await like.getActor(ctx);
699+
if (actor == null) return;
700+
const message = await createMessage(object, session, {});
701+
await this.onLike(
702+
session,
703+
{
704+
raw: like,
705+
id: like.id,
706+
actor,
707+
message,
708+
} satisfies Like<TContextData>,
709+
);
710+
}
711+
679712
dispatchNodeInfo(_ctx: Context<TContextData>): NodeInfo {
680713
return {
681714
software: this.software!,

src/bot.test.ts

+40-2
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,17 @@
1616
import { MemoryKvStore } from "@fedify/fedify/federation";
1717
import type { Actor } from "@fedify/fedify/vocab";
1818
import { assertEquals } from "@std/assert/equals";
19+
import type { BotImpl } from "./bot-impl.ts";
1920
import { createBot } from "./bot.ts";
2021
import type { FollowRequest } from "./follow.ts";
21-
import type { Message, MessageClass } from "./message.ts";
22+
import type { Message, MessageClass, SharedMessage } from "./message.ts";
23+
import type { Like } from "./reaction.ts";
2224
import type { Session } from "./session.ts";
2325

2426
Deno.test("createBot()", async () => {
2527
const kv = new MemoryKvStore();
26-
const bot = createBot({ kv, identifier: "bot-id", username: "bot" });
28+
const bot = createBot<void>({ kv, identifier: "bot-id", username: "bot" });
29+
const { impl } = bot as unknown as { impl: BotImpl<void> };
2730
const _federation = bot.federation;
2831
assertEquals(bot.identifier, "bot-id");
2932
const session = bot.getSession("https://example.com");
@@ -32,24 +35,59 @@ Deno.test("createBot()", async () => {
3235
function onFollow(_session: Session<void>, _followRequest: FollowRequest) {}
3336
bot.onFollow = onFollow;
3437
assertEquals(bot.onFollow, onFollow);
38+
assertEquals(impl.onFollow, onFollow);
3539

3640
function onUnfollow(_session: Session<void>, _follower: Actor) {}
3741
bot.onUnfollow = onUnfollow;
3842
assertEquals(bot.onUnfollow, onUnfollow);
43+
assertEquals(impl.onUnfollow, onUnfollow);
44+
45+
function onAcceptFollow(_session: Session<void>, _accepter: Actor) {}
46+
bot.onAcceptFollow = onAcceptFollow;
47+
assertEquals(bot.onAcceptFollow, onAcceptFollow);
48+
assertEquals(impl.onAcceptFollow, onAcceptFollow);
49+
50+
function onRejectFollow(_session: Session<void>, _rejecter: Actor) {}
51+
bot.onRejectFollow = onRejectFollow;
52+
assertEquals(bot.onRejectFollow, onRejectFollow);
53+
assertEquals(impl.onRejectFollow, onRejectFollow);
3954

4055
function onMention(
4156
_session: Session<void>,
4257
_message: Message<MessageClass, void>,
4358
) {}
4459
bot.onMention = onMention;
4560
assertEquals(bot.onMention, onMention);
61+
assertEquals(impl.onMention, onMention);
4662

4763
function onReply(
4864
_session: Session<void>,
4965
_message: Message<MessageClass, void>,
5066
) {}
5167
bot.onReply = onReply;
5268
assertEquals(bot.onReply, onReply);
69+
assertEquals(impl.onReply, onReply);
70+
71+
function onMessage(
72+
_session: Session<void>,
73+
_message: Message<MessageClass, void>,
74+
) {}
75+
bot.onMessage = onMessage;
76+
assertEquals(bot.onMessage, onMessage);
77+
assertEquals(impl.onMessage, onMessage);
78+
79+
function onSharedMessage(
80+
_session: Session<void>,
81+
_message: SharedMessage<MessageClass, void>,
82+
) {}
83+
bot.onSharedMessage = onSharedMessage;
84+
assertEquals(bot.onSharedMessage, onSharedMessage);
85+
assertEquals(impl.onSharedMessage, onSharedMessage);
86+
87+
function onLike(_session: Session<void>, _like: Like<void>) {}
88+
bot.onLike = onLike;
89+
assertEquals(bot.onLike, onLike);
90+
assertEquals(impl.onLike, onLike);
5391

5492
const response = await bot.fetch(
5593
new Request(

src/bot.ts

+38-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { BotImpl } from "./bot-impl.ts";
2525
import type {
2626
AcceptEventHandler,
2727
FollowEventHandler,
28+
LikeEventHandler,
2829
MentionEventHandler,
2930
MessageEventHandler,
3031
RejectEventHandler,
@@ -127,6 +128,11 @@ export interface Bot<TContextData> {
127128
* your bot needs to follow others first.
128129
*/
129130
onSharedMessage?: SharedMessageEventHandler<TContextData>;
131+
132+
/**
133+
* An event handler for a like of a message.
134+
*/
135+
onLike?: LikeEventHandler<TContextData>;
130136
}
131137

132138
/**
@@ -327,6 +333,7 @@ export function createBot<TContextData = void>(
327333
// we wrap a BotImpl instance with a plain object.
328334
// See also https://github.com/denoland/deno/issues/24062
329335
const wrapper = {
336+
impl: bot,
330337
get federation() {
331338
return bot.federation;
332339
},
@@ -352,6 +359,18 @@ export function createBot<TContextData = void>(
352359
set onUnfollow(value) {
353360
bot.onUnfollow = value;
354361
},
362+
get onAcceptFollow() {
363+
return bot.onAcceptFollow;
364+
},
365+
set onAcceptFollow(value) {
366+
bot.onAcceptFollow = value;
367+
},
368+
get onRejectFollow() {
369+
return bot.onRejectFollow;
370+
},
371+
set onRejectFollow(value) {
372+
bot.onRejectFollow = value;
373+
},
355374
get onMention() {
356375
return bot.onMention;
357376
},
@@ -364,7 +383,25 @@ export function createBot<TContextData = void>(
364383
set onReply(value) {
365384
bot.onReply = value;
366385
},
367-
} satisfies Bot<TContextData>;
386+
get onMessage() {
387+
return bot.onMessage;
388+
},
389+
set onMessage(value) {
390+
bot.onMessage = value;
391+
},
392+
get onSharedMessage() {
393+
return bot.onSharedMessage;
394+
},
395+
set onSharedMessage(value) {
396+
bot.onSharedMessage = value;
397+
},
398+
get onLike() {
399+
return bot.onLike;
400+
},
401+
set onLike(value) {
402+
bot.onLike = value;
403+
},
404+
} satisfies Bot<TContextData> & { impl: BotImpl<TContextData> };
368405
// @ts-ignore: the wrapper implements BotWithVoidContextData
369406
return wrapper;
370407
}

src/events.ts

+12
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import type { Actor } from "@fedify/fedify/vocab";
1717
import type { FollowRequest } from "./follow.ts";
1818
import type { Message, MessageClass, SharedMessage } from "./message.ts";
19+
import type { Like } from "./reaction.ts";
1920
import type { Session } from "./session.ts";
2021

2122
/**
@@ -107,3 +108,14 @@ export type SharedMessageEventHandler<TContextData> = (
107108
session: Session<TContextData>,
108109
message: SharedMessage<MessageClass, TContextData>,
109110
) => void | Promise<void>;
111+
112+
/**
113+
* An event handler for a like of a message.
114+
* @typeParam TContextData The type of the context data.
115+
* @param session The session of the bot.
116+
* @param like The like activity of the message.
117+
*/
118+
export type LikeEventHandler<TContextData> = (
119+
session: Session<TContextData>,
120+
like: Like<TContextData>,
121+
) => void | Promise<void>;

0 commit comments

Comments
 (0)