Skip to content

Commit 3e3665c

Browse files
committed
Receiving emoji reactions
1 parent c997c6a commit 3e3665c

File tree

7 files changed

+417
-11
lines changed

7 files changed

+417
-11
lines changed

.vscode/settings.json

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
"unfollows",
7575
"unliked",
7676
"unreact",
77+
"unreacted",
7778
"uuidv7",
7879
"vitepress"
7980
]

CHANGES.md

+4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ To be released.
2929
- Added `Message.react()` method.
3030
- Added `Reaction` interface.
3131
- Added `AuthorizedReaction` interface.
32+
- Added `Bot.onReact` event.
33+
- Added `Bot.onUnreact` event.
34+
- Added `ReactionEventHandler` type.
35+
- Added `UndoneReactionEventHandler` type.
3236

3337
- Added `SessionGetOutboxOptions` interface.
3438

docs/concepts/events.md

+49
Original file line numberDiff line numberDiff line change
@@ -317,3 +317,52 @@ bot.onUnlike = async (session, like) => {
317317
);
318318
};
319319
~~~~
320+
321+
322+
Emoji reaction
323+
--------------
324+
325+
*This API is available since BotKit 0.2.0.*
326+
327+
The `~Bot.onReact` event handler is called when someone reacts with an emoji to
328+
messages on your bot or actors your bot follows. It receives a `Reaction`
329+
object, which represents the reaction activity, as the second argument.
330+
331+
The following is an example of a reaction event handler that sends a direct
332+
message when someone reacts with an emoji to a message on your bot:
333+
334+
~~~~ typescript
335+
bot.onReact = async (session, reaction) => {
336+
if (reaction.message.actor.id?.href !== session.actorId.href) return;
337+
await session.publish(
338+
text`Thanks for reacting with ${reaction.emoji} to my message, ${reaction.actor}!`,
339+
{ visibility: "direct" },
340+
);
341+
};
342+
~~~~
343+
344+
345+
Undoing emoji reaction
346+
----------------------
347+
348+
*This API is available since BotKit 0.2.0.*
349+
350+
The `~Bot.onUnreact` event handler is called when someone removes an emoji
351+
reaction from messages on your bot or actors your bot follows. It receives
352+
a `Reaction` object, which represents the emoji reaction activity which was
353+
undone, as the second argument.
354+
355+
The following is an example of an unreact event handler that sends a direct
356+
message when someone removes a heart reaction from a message on your bot:
357+
358+
~~~~ typescript
359+
bot.onUnreact = async (session, reaction) => {
360+
if (reaction.message.actor.id?.href !== session.actorId.href) return;
361+
if (reaction.emoji === "❤️") {
362+
await session.publish(
363+
text`I see you took back your heart reaction, ${reaction.actor}.`,
364+
{ visibility: "direct" },
365+
);
366+
}
367+
};
368+
~~~~

src/bot-impl.test.ts

+210-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
Create,
2424
CryptographicKey,
2525
Emoji,
26+
EmojiReact,
2627
Follow,
2728
Image,
2829
Like as RawLike,
@@ -47,7 +48,7 @@ import { parseSemVer } from "./bot.ts";
4748
import type { CustomEmoji } from "./emoji.ts";
4849
import type { FollowRequest } from "./follow.ts";
4950
import type { Message, MessageClass, SharedMessage } from "./message.ts";
50-
import type { Like } from "./reaction.ts";
51+
import type { Like, Reaction } from "./reaction.ts";
5152
import { MemoryRepository } from "./repository.ts";
5253
import { SessionImpl } from "./session-impl.ts";
5354
import type { Session } from "./session.ts";
@@ -1902,6 +1903,212 @@ Deno.test("BotImpl.onUnliked()", async () => {
19021903
assertEquals(likes, []);
19031904
});
19041905

1906+
Deno.test("BotImpl.onReacted()", async () => {
1907+
const bot = new BotImpl<void>({
1908+
kv: new MemoryKvStore(),
1909+
username: "bot",
1910+
});
1911+
const reactions: [Session<void>, Reaction<void>][] = [];
1912+
bot.onReact = (session, reaction) =>
1913+
void (reactions.push([session, reaction]));
1914+
const ctx = createMockInboxContext(bot, "https://example.com", "bot");
1915+
1916+
// Test with Like containing an emoji name
1917+
const rawLike = new RawLike({
1918+
id: new URL("https://example.com/ap/actor/bot/like/1"),
1919+
actor: new URL("https://example.com/ap/actor/bot"),
1920+
name: ":heart:",
1921+
object: new Note({
1922+
id: new URL("https://example.com/ap/actor/bot/note/1"),
1923+
attribution: new URL("https://example.com/ap/actor/bot"),
1924+
to: PUBLIC_COLLECTION,
1925+
cc: new URL("https://example.com/ap/actor/bot/followers"),
1926+
content: "Hello, world!",
1927+
}),
1928+
tags: [
1929+
new Emoji({
1930+
id: new URL("https://example.com/ap/emoji/heart"),
1931+
name: ":heart:",
1932+
icon: new Image({
1933+
mediaType: "image/png",
1934+
url: new URL("https://example.com/emoji/heart.png"),
1935+
}),
1936+
}),
1937+
],
1938+
});
1939+
1940+
await bot.onReacted(ctx, rawLike);
1941+
assertEquals(reactions.length, 1);
1942+
const [session, reaction] = reactions[0];
1943+
assertEquals(session.bot, bot);
1944+
assertEquals(session.context, ctx);
1945+
assertEquals(reaction.raw, rawLike);
1946+
assertEquals(reaction.id, rawLike.id);
1947+
assertEquals(reaction.actor.id, rawLike.actorId);
1948+
assertEquals(reaction.message.id, rawLike.objectId);
1949+
assertInstanceOf(reaction.emoji, Emoji);
1950+
assertEquals(reaction.emoji.name, ":heart:");
1951+
assertEquals(ctx.sentActivities, []);
1952+
assertEquals(ctx.forwardedRecipients, []);
1953+
1954+
// Test with EmojiReact
1955+
reactions.pop();
1956+
const emojiReact = new EmojiReact({
1957+
id: new URL("https://example.com/ap/actor/bot/react/1"),
1958+
actor: new URL("https://example.com/ap/actor/bot"),
1959+
name: ":thumbsup:",
1960+
object: new Note({
1961+
id: new URL("https://example.com/ap/actor/bot/note/1"),
1962+
attribution: new URL("https://example.com/ap/actor/bot"),
1963+
to: PUBLIC_COLLECTION,
1964+
cc: new URL("https://example.com/ap/actor/bot/followers"),
1965+
content: "Hello, world!",
1966+
}),
1967+
tags: [
1968+
new Emoji({
1969+
id: new URL("https://example.com/ap/emoji/thumbsup"),
1970+
name: ":thumbsup:",
1971+
icon: new Image({
1972+
mediaType: "image/png",
1973+
url: new URL("https://example.com/emoji/thumbsup.png"),
1974+
}),
1975+
}),
1976+
],
1977+
});
1978+
1979+
await bot.onReacted(ctx, emojiReact);
1980+
assertEquals(reactions.length, 1);
1981+
const [session2, reaction2] = reactions[0];
1982+
assertEquals(session2.bot, bot);
1983+
assertEquals(session2.context, ctx);
1984+
assertEquals(reaction2.raw, emojiReact);
1985+
assertEquals(reaction2.id, emojiReact.id);
1986+
assertEquals(reaction2.actor.id, emojiReact.actorId);
1987+
assertEquals(reaction2.message.id, emojiReact.objectId);
1988+
assertInstanceOf(reaction2.emoji, Emoji);
1989+
assertEquals(reaction2.emoji.name, ":thumbsup:");
1990+
assertEquals(ctx.sentActivities, []);
1991+
assertEquals(ctx.forwardedRecipients, []);
1992+
});
1993+
1994+
Deno.test("BotImpl.onUnreacted()", async () => {
1995+
const bot = new BotImpl<void>({
1996+
kv: new MemoryKvStore(),
1997+
username: "bot",
1998+
});
1999+
const reactions: [Session<void>, Reaction<void>][] = [];
2000+
bot.onUnreact = (session, reaction) =>
2001+
void (reactions.push([session, reaction]));
2002+
const ctx = createMockInboxContext(bot, "https://example.com", "bot");
2003+
2004+
// Test with Like containing an emoji name
2005+
const rawLike = new RawLike({
2006+
id: new URL("https://example.com/ap/actor/bot/like/1"),
2007+
actor: new URL("https://example.com/ap/actor/bot"),
2008+
name: ":heart:",
2009+
object: new Note({
2010+
id: new URL("https://example.com/ap/actor/bot/note/1"),
2011+
attribution: new URL("https://example.com/ap/actor/bot"),
2012+
to: PUBLIC_COLLECTION,
2013+
cc: new URL("https://example.com/ap/actor/bot/followers"),
2014+
content: "Hello, world!",
2015+
}),
2016+
tags: [
2017+
new Emoji({
2018+
id: new URL("https://example.com/ap/emoji/heart"),
2019+
name: ":heart:",
2020+
icon: new Image({
2021+
mediaType: "image/png",
2022+
url: new URL("https://example.com/emoji/heart.png"),
2023+
}),
2024+
}),
2025+
],
2026+
});
2027+
2028+
const undo = new Undo({
2029+
id: new URL("https://example.com/ap/actor/bot/unreact/1"),
2030+
actor: new URL("https://example.com/ap/actor/bot"),
2031+
object: rawLike,
2032+
});
2033+
2034+
await bot.onUnreacted(ctx, undo);
2035+
assertEquals(reactions.length, 1);
2036+
const [session, reaction] = reactions[0];
2037+
assertEquals(session.bot, bot);
2038+
assertEquals(session.context, ctx);
2039+
assertEquals(reaction.raw, rawLike);
2040+
assertEquals(reaction.id, rawLike.id);
2041+
assertEquals(reaction.actor.id, rawLike.actorId);
2042+
assertEquals(reaction.message.id, rawLike.objectId);
2043+
assertInstanceOf(reaction.emoji, Emoji);
2044+
assertEquals(reaction.emoji.name, ":heart:");
2045+
assertEquals(ctx.sentActivities, []);
2046+
assertEquals(ctx.forwardedRecipients, []);
2047+
2048+
// Test with EmojiReact
2049+
reactions.pop();
2050+
const emojiReact = new EmojiReact({
2051+
id: new URL("https://example.com/ap/actor/bot/react/1"),
2052+
actor: new URL("https://example.com/ap/actor/bot"),
2053+
name: ":thumbsup:",
2054+
object: new Note({
2055+
id: new URL("https://example.com/ap/actor/bot/note/1"),
2056+
attribution: new URL("https://example.com/ap/actor/bot"),
2057+
to: PUBLIC_COLLECTION,
2058+
cc: new URL("https://example.com/ap/actor/bot/followers"),
2059+
content: "Hello, world!",
2060+
}),
2061+
tags: [
2062+
new Emoji({
2063+
id: new URL("https://example.com/ap/emoji/thumbsup"),
2064+
name: ":thumbsup:",
2065+
icon: new Image({
2066+
mediaType: "image/png",
2067+
url: new URL("https://example.com/emoji/thumbsup.png"),
2068+
}),
2069+
}),
2070+
],
2071+
});
2072+
2073+
const undoEmojiReact = new Undo({
2074+
id: new URL("https://example.com/ap/actor/bot/unreact/2"),
2075+
actor: new URL("https://example.com/ap/actor/bot"),
2076+
object: emojiReact,
2077+
});
2078+
2079+
await bot.onUnreacted(ctx, undoEmojiReact);
2080+
assertEquals(reactions.length, 1);
2081+
const [session2, reaction2] = reactions[0];
2082+
assertEquals(session2.bot, bot);
2083+
assertEquals(session2.context, ctx);
2084+
assertEquals(reaction2.raw, emojiReact);
2085+
assertEquals(reaction2.id, emojiReact.id);
2086+
assertEquals(reaction2.actor.id, emojiReact.actorId);
2087+
assertEquals(reaction2.message.id, emojiReact.objectId);
2088+
assertInstanceOf(reaction2.emoji, Emoji);
2089+
assertEquals(reaction2.emoji.name, ":thumbsup:");
2090+
assertEquals(ctx.sentActivities, []);
2091+
assertEquals(ctx.forwardedRecipients, []);
2092+
2093+
// Test with mismatched actor
2094+
reactions.pop();
2095+
const invalidUndo = undoEmojiReact.clone({
2096+
actor: new URL("https://example.com/ap/actor/another"),
2097+
});
2098+
await bot.onUnreacted(ctx, invalidUndo);
2099+
assertEquals(reactions, []);
2100+
2101+
// Test with non-reaction object
2102+
reactions.pop();
2103+
const nonReactionUndo = new Undo({
2104+
id: new URL("https://example.com/ap/actor/bot/unreact/3"),
2105+
actor: new URL("https://example.com/ap/actor/bot"),
2106+
object: new Note({}),
2107+
});
2108+
await bot.onUnreacted(ctx, nonReactionUndo);
2109+
assertEquals(reactions, []);
2110+
});
2111+
19052112
Deno.test("BotImpl.dispatchNodeInfo()", () => {
19062113
const bot = new BotImpl<void>({
19072114
kv: new MemoryKvStore(),
@@ -2176,3 +2383,5 @@ function createMockInboxContext(
21762383
};
21772384
return ctx;
21782385
}
2386+
2387+
// cSpell: ignore thumbsup

0 commit comments

Comments
 (0)