@fedify/botkit 0.3.0-dev.108
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +75 -0
- package/dist/bot-impl.d.ts +111 -0
- package/dist/bot-impl.d.ts.map +1 -0
- package/dist/bot-impl.js +602 -0
- package/dist/bot-impl.js.map +1 -0
- package/dist/bot-impl.test.d.ts +2 -0
- package/dist/bot-impl.test.js +1642 -0
- package/dist/bot-impl.test.js.map +1 -0
- package/dist/bot.d.ts +270 -0
- package/dist/bot.d.ts.map +1 -0
- package/dist/bot.js +118 -0
- package/dist/bot.js.map +1 -0
- package/dist/bot.test.d.ts +2 -0
- package/dist/bot.test.js +80 -0
- package/dist/bot.test.js.map +1 -0
- package/dist/components/Layout.d.ts +26 -0
- package/dist/components/Layout.d.ts.map +1 -0
- package/dist/components/Layout.js +36 -0
- package/dist/components/Layout.js.map +1 -0
- package/dist/components/Message.d.ts +21 -0
- package/dist/components/Message.d.ts.map +1 -0
- package/dist/components/Message.js +99 -0
- package/dist/components/Message.js.map +1 -0
- package/dist/components/Message.test.d.ts +2 -0
- package/dist/components/Message.test.js +32 -0
- package/dist/components/Message.test.js.map +1 -0
- package/dist/deno.js +73 -0
- package/dist/deno.js.map +1 -0
- package/dist/emoji.d.ts +87 -0
- package/dist/emoji.d.ts.map +1 -0
- package/dist/emoji.js +37 -0
- package/dist/emoji.js.map +1 -0
- package/dist/emoji.test.d.ts +2 -0
- package/dist/emoji.test.js +109 -0
- package/dist/emoji.test.js.map +1 -0
- package/dist/events.d.ts +110 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +4 -0
- package/dist/follow-impl.d.ts +23 -0
- package/dist/follow-impl.d.ts.map +1 -0
- package/dist/follow-impl.js +51 -0
- package/dist/follow-impl.js.map +1 -0
- package/dist/follow-impl.test.d.ts +2 -0
- package/dist/follow-impl.test.js +110 -0
- package/dist/follow-impl.test.js.map +1 -0
- package/dist/follow.d.ts +44 -0
- package/dist/follow.d.ts.map +1 -0
- package/dist/follow.js +4 -0
- package/dist/message-impl.d.ts +54 -0
- package/dist/message-impl.d.ts.map +1 -0
- package/dist/message-impl.js +439 -0
- package/dist/message-impl.js.map +1 -0
- package/dist/message-impl.test.d.ts +2 -0
- package/dist/message-impl.test.js +519 -0
- package/dist/message-impl.test.js.map +1 -0
- package/dist/message.d.ts +223 -0
- package/dist/message.d.ts.map +1 -0
- package/dist/message.js +8 -0
- package/dist/mod.d.ts +13 -0
- package/dist/mod.js +13 -0
- package/dist/pages.d.ts +20 -0
- package/dist/pages.d.ts.map +1 -0
- package/dist/pages.js +361 -0
- package/dist/pages.js.map +1 -0
- package/dist/reaction.d.ts +90 -0
- package/dist/reaction.d.ts.map +1 -0
- package/dist/reaction.js +7 -0
- package/dist/repository.d.ts +323 -0
- package/dist/repository.d.ts.map +1 -0
- package/dist/repository.js +483 -0
- package/dist/repository.js.map +1 -0
- package/dist/repository.test.d.ts +2 -0
- package/dist/repository.test.js +336 -0
- package/dist/repository.test.js.map +1 -0
- package/dist/session-impl.d.ts +32 -0
- package/dist/session-impl.d.ts.map +1 -0
- package/dist/session-impl.js +195 -0
- package/dist/session-impl.js.map +1 -0
- package/dist/session-impl.test.d.ts +20 -0
- package/dist/session-impl.test.d.ts.map +1 -0
- package/dist/session-impl.test.js +464 -0
- package/dist/session-impl.test.js.map +1 -0
- package/dist/session.d.ts +139 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +4 -0
- package/dist/text.d.ts +391 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +640 -0
- package/dist/text.js.map +1 -0
- package/dist/text.test.d.ts +2 -0
- package/dist/text.test.js +473 -0
- package/dist/text.test.js.map +1 -0
- package/package.json +137 -0
package/dist/emoji.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Temporal, toTemporalInstant } from "@js-temporal/polyfill";
|
|
2
|
+
Date.prototype.toTemporalInstant = toTemporalInstant;
|
|
3
|
+
import { Session } from "./session.js";
|
|
4
|
+
import { Emoji as Emoji$1 } from "@fedify/fedify/vocab";
|
|
5
|
+
|
|
6
|
+
//#region src/emoji.d.ts
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A branded type for a single emoji character (more exactly, a single
|
|
10
|
+
* Unicode grapheme cluster of emoji). This is used to represent a single emoji
|
|
11
|
+
* in a string format. It is not a full-fledged emoji object, but rather a
|
|
12
|
+
* string that is guaranteed to be a single emoji.
|
|
13
|
+
*
|
|
14
|
+
* You can narrow a string to an {@link Emoji} type using the {@link isEmoji}
|
|
15
|
+
* predicate function.
|
|
16
|
+
* @since 0.2.0
|
|
17
|
+
*/
|
|
18
|
+
type Emoji = string & {
|
|
19
|
+
readonly __emoji: unique symbol;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* A type guard that checks if a value is a single emoji character.
|
|
23
|
+
* @param value The value to check.
|
|
24
|
+
* @returns `true` if the value is a single emoji character, `false` otherwise.
|
|
25
|
+
* @since 0.2.0
|
|
26
|
+
*/
|
|
27
|
+
declare function isEmoji(value: unknown): value is Emoji;
|
|
28
|
+
/**
|
|
29
|
+
* A tagged template literal function that creates an {@link Emoji} from
|
|
30
|
+
* a string. It is a simple wrapper around the `String.raw` function,
|
|
31
|
+
* but it also checks if the resulting string is a valid emoji.
|
|
32
|
+
* @param strings The template strings.
|
|
33
|
+
* @param values The values to interpolate into the template strings.
|
|
34
|
+
* @returns The resulting {@link Emoji} value.
|
|
35
|
+
* @throws {TypeError} If the resulting string is not a valid emoji.
|
|
36
|
+
* @since 0.2.0
|
|
37
|
+
*/
|
|
38
|
+
declare function emoji(strings: TemplateStringsArray, ...values: unknown[]): Emoji;
|
|
39
|
+
/**
|
|
40
|
+
* The common interface for defining custom emojis.
|
|
41
|
+
* @since 0.2.0
|
|
42
|
+
*/
|
|
43
|
+
interface CustomEmojiBase {
|
|
44
|
+
/**
|
|
45
|
+
* The media type of the emoji. It has to start with `image/`.
|
|
46
|
+
*/
|
|
47
|
+
readonly type: `image/${string}`;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The interface for defining custom emojis from a remote image URL.
|
|
51
|
+
* @since 0.2.0
|
|
52
|
+
*/
|
|
53
|
+
interface CustomEmojiFromUrl extends CustomEmojiBase {
|
|
54
|
+
/**
|
|
55
|
+
* The URL of the remote image.
|
|
56
|
+
*/
|
|
57
|
+
readonly url: string | URL;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The interface for defining custom emojis from a local image file.
|
|
61
|
+
* @since 0.2.0
|
|
62
|
+
*/
|
|
63
|
+
interface CustomEmojiFromFile extends CustomEmojiBase {
|
|
64
|
+
/**
|
|
65
|
+
* The path to the local image file.
|
|
66
|
+
*/
|
|
67
|
+
readonly file: string | URL;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A definition of a custom emoji. It can be either a remote image URL or
|
|
71
|
+
* a local image file.
|
|
72
|
+
* @since 0.2.0
|
|
73
|
+
*/
|
|
74
|
+
type CustomEmoji = CustomEmojiFromUrl | CustomEmojiFromFile;
|
|
75
|
+
/**
|
|
76
|
+
* A deferred `Emoji` (provided by Fedify), which is a function that
|
|
77
|
+
* takes a {@link Session} and returns an `Emoji`. This is useful for
|
|
78
|
+
* creating emojis that depend on the session data.
|
|
79
|
+
* @since 0.2.0
|
|
80
|
+
* @param TContextData The type of the context data.
|
|
81
|
+
* @return The `Emoji` object.
|
|
82
|
+
*/
|
|
83
|
+
type DeferredCustomEmoji<TContextData> = (session: Session<TContextData>) => Emoji$1;
|
|
84
|
+
//# sourceMappingURL=emoji.d.ts.map
|
|
85
|
+
//#endregion
|
|
86
|
+
export { CustomEmoji, CustomEmojiBase, CustomEmojiFromFile, CustomEmojiFromUrl, DeferredCustomEmoji, Emoji, emoji, isEmoji };
|
|
87
|
+
//# sourceMappingURL=emoji.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emoji.d.ts","names":[],"sources":["../src/emoji.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;AA4BA;AAQA;AAsBA;;AACW,KA/BC,KAAA,GA+BD,MAAA,GAAA;EAAoB,SAE5B,OAAA,EAAA,OAAA,MAAA;AAAK,CAAA;AAYR;AAWA;;;;AAA2D;AAW1C,iBA3DD,OAAA,CA2DqB,KAAA,EAAA,OAAA,CAAA,EAAA,KAAA,IA3Da,KA2Db;;;;AAAuB;AAY5D;;;;AAAkE;AAUlE;AAA+B,iBA3Df,KAAA,CA2De,OAAA,EA1DpB,oBA0DoB,EAAA,GAAA,MAAA,EAAA,OAAA,EAAA,CAAA,EAxD5B,KAwD4B;;;;AAEf;UA9CC,eAAA;;;;;;;;;;UAWA,kBAAA,SAA2B;;;;yBAInB;;;;;;UAOR,mBAAA,SAA4B;;;;0BAInB;;;;;;;KAQd,WAAA,GAAc,qBAAqB;;;;;;;;;KAUnC,8CACD,QAAQ,kBACd"}
|
package/dist/emoji.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
|
|
2
|
+
import { Temporal, toTemporalInstant } from "@js-temporal/polyfill";
|
|
3
|
+
Date.prototype.toTemporalInstant = toTemporalInstant;
|
|
4
|
+
|
|
5
|
+
//#region src/emoji.ts
|
|
6
|
+
/**
|
|
7
|
+
* A type guard that checks if a value is a single emoji character.
|
|
8
|
+
* @param value The value to check.
|
|
9
|
+
* @returns `true` if the value is a single emoji character, `false` otherwise.
|
|
10
|
+
* @since 0.2.0
|
|
11
|
+
*/
|
|
12
|
+
function isEmoji(value) {
|
|
13
|
+
if (typeof value !== "string") return false;
|
|
14
|
+
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
|
|
15
|
+
const segments = [...segmenter.segment(value)];
|
|
16
|
+
if (segments.length !== 1) return false;
|
|
17
|
+
return /\p{Emoji}/u.test(segments[0].segment);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A tagged template literal function that creates an {@link Emoji} from
|
|
21
|
+
* a string. It is a simple wrapper around the `String.raw` function,
|
|
22
|
+
* but it also checks if the resulting string is a valid emoji.
|
|
23
|
+
* @param strings The template strings.
|
|
24
|
+
* @param values The values to interpolate into the template strings.
|
|
25
|
+
* @returns The resulting {@link Emoji} value.
|
|
26
|
+
* @throws {TypeError} If the resulting string is not a valid emoji.
|
|
27
|
+
* @since 0.2.0
|
|
28
|
+
*/
|
|
29
|
+
function emoji(strings, ...values) {
|
|
30
|
+
const result = String.raw(strings, ...values);
|
|
31
|
+
if (!isEmoji(result)) throw new TypeError(`Invalid emoji: ${result}`);
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
//#endregion
|
|
36
|
+
export { emoji, isEmoji };
|
|
37
|
+
//# sourceMappingURL=emoji.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emoji.js","names":["value: unknown","strings: TemplateStringsArray"],"sources":["../src/emoji.ts"],"sourcesContent":["// BotKit by Fedify: A framework for creating ActivityPub bots\n// Copyright (C) 2025 Hong Minhee <https://hongminhee.org/>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program. If not, see <https://www.gnu.org/licenses/>.\nimport type { Emoji as EmojiObject } from \"@fedify/fedify/vocab\";\nimport type { Session } from \"./session.ts\";\n\n/**\n * A branded type for a single emoji character (more exactly, a single\n * Unicode grapheme cluster of emoji). This is used to represent a single emoji\n * in a string format. It is not a full-fledged emoji object, but rather a\n * string that is guaranteed to be a single emoji.\n *\n * You can narrow a string to an {@link Emoji} type using the {@link isEmoji}\n * predicate function.\n * @since 0.2.0\n */\nexport type Emoji = string & { readonly __emoji: unique symbol };\n\n/**\n * A type guard that checks if a value is a single emoji character.\n * @param value The value to check.\n * @returns `true` if the value is a single emoji character, `false` otherwise.\n * @since 0.2.0\n */\nexport function isEmoji(value: unknown): value is Emoji {\n if (typeof value !== \"string\") return false;\n\n // First check if we have exactly one grapheme cluster\n const segmenter = new Intl.Segmenter(\"en\", { granularity: \"grapheme\" });\n const segments = [...segmenter.segment(value)];\n if (segments.length !== 1) return false;\n\n // Then check if this grapheme cluster has the emoji property\n return /\\p{Emoji}/u.test(segments[0].segment);\n}\n\n/**\n * A tagged template literal function that creates an {@link Emoji} from\n * a string. It is a simple wrapper around the `String.raw` function,\n * but it also checks if the resulting string is a valid emoji.\n * @param strings The template strings.\n * @param values The values to interpolate into the template strings.\n * @returns The resulting {@link Emoji} value.\n * @throws {TypeError} If the resulting string is not a valid emoji.\n * @since 0.2.0\n */\nexport function emoji(\n strings: TemplateStringsArray,\n ...values: unknown[]\n): Emoji {\n const result = String.raw(strings, ...values);\n if (!isEmoji(result)) {\n throw new TypeError(`Invalid emoji: ${result}`);\n }\n return result;\n}\n\n/**\n * The common interface for defining custom emojis.\n * @since 0.2.0\n */\nexport interface CustomEmojiBase {\n /**\n * The media type of the emoji. It has to start with `image/`.\n */\n readonly type: `image/${string}`;\n}\n\n/**\n * The interface for defining custom emojis from a remote image URL.\n * @since 0.2.0\n */\nexport interface CustomEmojiFromUrl extends CustomEmojiBase {\n /**\n * The URL of the remote image.\n */\n readonly url: string | URL;\n}\n\n/**\n * The interface for defining custom emojis from a local image file.\n * @since 0.2.0\n */\nexport interface CustomEmojiFromFile extends CustomEmojiBase {\n /**\n * The path to the local image file.\n */\n readonly file: string | URL;\n}\n\n/**\n * A definition of a custom emoji. It can be either a remote image URL or\n * a local image file.\n * @since 0.2.0\n */\nexport type CustomEmoji = CustomEmojiFromUrl | CustomEmojiFromFile;\n\n/**\n * A deferred `Emoji` (provided by Fedify), which is a function that\n * takes a {@link Session} and returns an `Emoji`. This is useful for\n * creating emojis that depend on the session data.\n * @since 0.2.0\n * @param TContextData The type of the context data.\n * @return The `Emoji` object.\n */\nexport type DeferredCustomEmoji<TContextData> = (\n session: Session<TContextData>,\n) => EmojiObject;\n"],"mappings":";;;;;;;;;;;AAoCA,SAAgB,QAAQA,OAAgC;AACtD,YAAW,UAAU,SAAU,QAAO;CAGtC,MAAM,YAAY,IAAI,KAAK,UAAU,MAAM,EAAE,aAAa,WAAY;CACtE,MAAM,WAAW,CAAC,GAAG,UAAU,QAAQ,MAAM,AAAC;AAC9C,KAAI,SAAS,WAAW,EAAG,QAAO;AAGlC,QAAO,aAAa,KAAK,SAAS,GAAG,QAAQ;AAC9C;;;;;;;;;;;AAYD,SAAgB,MACdC,SACA,GAAG,QACI;CACP,MAAM,SAAS,OAAO,IAAI,SAAS,GAAG,OAAO;AAC7C,MAAK,QAAQ,OAAO,CAClB,OAAM,IAAI,WAAW,iBAAiB,OAAO;AAE/C,QAAO;AACR"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
|
|
2
|
+
import { Temporal, toTemporalInstant } from "@js-temporal/polyfill";
|
|
3
|
+
Date.prototype.toTemporalInstant = toTemporalInstant;
|
|
4
|
+
|
|
5
|
+
import { emoji, isEmoji } from "./emoji.js";
|
|
6
|
+
import assert from "node:assert";
|
|
7
|
+
import { test } from "node:test";
|
|
8
|
+
|
|
9
|
+
//#region src/emoji.test.ts
|
|
10
|
+
test("isEmoji() with valid emojis", () => {
|
|
11
|
+
const validEmojis = [
|
|
12
|
+
"😀",
|
|
13
|
+
"👍",
|
|
14
|
+
"🚀",
|
|
15
|
+
"🏳️🌈",
|
|
16
|
+
"👨👩👧👦",
|
|
17
|
+
"👩🏽🔬",
|
|
18
|
+
"🧘🏻♀️",
|
|
19
|
+
"🤦♂️",
|
|
20
|
+
"🇯🇵"
|
|
21
|
+
];
|
|
22
|
+
for (const emoji$1 of validEmojis) assert.ok(isEmoji(emoji$1), `Expected '${emoji$1}' to be recognized as an emoji`);
|
|
23
|
+
});
|
|
24
|
+
test("isEmoji() with invalid inputs", () => {
|
|
25
|
+
const invalidInputs = [
|
|
26
|
+
"😀😀",
|
|
27
|
+
"👍🏻👎🏻",
|
|
28
|
+
"hello",
|
|
29
|
+
"a",
|
|
30
|
+
"hi😀",
|
|
31
|
+
"👍awesome",
|
|
32
|
+
"",
|
|
33
|
+
42,
|
|
34
|
+
null,
|
|
35
|
+
void 0,
|
|
36
|
+
true,
|
|
37
|
+
false,
|
|
38
|
+
{},
|
|
39
|
+
[],
|
|
40
|
+
/* @__PURE__ */ new Date()
|
|
41
|
+
];
|
|
42
|
+
for (const input of invalidInputs) assert.strictEqual(isEmoji(input), false, `Expected '${input}' not to be recognized as an emoji`);
|
|
43
|
+
});
|
|
44
|
+
test("isEmoji() with additional edge cases", () => {
|
|
45
|
+
const edgeCaseEmojis = [
|
|
46
|
+
"5️⃣",
|
|
47
|
+
"❤️",
|
|
48
|
+
"☺️",
|
|
49
|
+
"👩🦰",
|
|
50
|
+
"🏊♀️",
|
|
51
|
+
"🧙♂️",
|
|
52
|
+
"🔢",
|
|
53
|
+
"↔️",
|
|
54
|
+
"📧",
|
|
55
|
+
"📱"
|
|
56
|
+
];
|
|
57
|
+
for (const emoji$1 of edgeCaseEmojis) assert.ok(isEmoji(emoji$1), `Expected '${emoji$1}' to be recognized as an emoji`);
|
|
58
|
+
});
|
|
59
|
+
test("isEmoji() with tricky invalid inputs", () => {
|
|
60
|
+
const trickyInvalidInputs = [
|
|
61
|
+
" 😀",
|
|
62
|
+
"😀 ",
|
|
63
|
+
"😀",
|
|
64
|
+
"🏴72",
|
|
65
|
+
"️",
|
|
66
|
+
"",
|
|
67
|
+
"♀️♂️"
|
|
68
|
+
];
|
|
69
|
+
for (const input of trickyInvalidInputs) assert.strictEqual(isEmoji(input), false, `Expected '${input}' not to be recognized as an emoji`);
|
|
70
|
+
});
|
|
71
|
+
test("emoji() tagged template function with valid emojis", () => {
|
|
72
|
+
const validEmojis = [
|
|
73
|
+
emoji`😀`,
|
|
74
|
+
emoji`👍`,
|
|
75
|
+
emoji`🚀`,
|
|
76
|
+
emoji`🏳️🌈`,
|
|
77
|
+
emoji`👨👩👧👦`,
|
|
78
|
+
emoji`👩🏽🔬`,
|
|
79
|
+
emoji`🧘🏻♀️`,
|
|
80
|
+
emoji`🇯🇵`
|
|
81
|
+
];
|
|
82
|
+
for (const emojiValue of validEmojis) assert.ok(isEmoji(emojiValue));
|
|
83
|
+
});
|
|
84
|
+
test("emoji() tagged template function with interpolation", () => {
|
|
85
|
+
const rocket = "🚀";
|
|
86
|
+
const result = emoji`${rocket}`;
|
|
87
|
+
assert.ok(isEmoji(result));
|
|
88
|
+
assert.strictEqual(result, "🚀");
|
|
89
|
+
});
|
|
90
|
+
test("emoji() throws with invalid inputs", () => {
|
|
91
|
+
const invalidInputs = [
|
|
92
|
+
() => emoji`😀😀`,
|
|
93
|
+
() => emoji`hi😀`,
|
|
94
|
+
() => emoji`👍awesome`,
|
|
95
|
+
() => emoji` 😀`,
|
|
96
|
+
() => emoji`😀 `,
|
|
97
|
+
() => emoji``
|
|
98
|
+
];
|
|
99
|
+
for (const fn of invalidInputs) try {
|
|
100
|
+
fn();
|
|
101
|
+
assert.fail("Expected function to throw TypeError");
|
|
102
|
+
} catch (error) {
|
|
103
|
+
assert.ok(error instanceof TypeError);
|
|
104
|
+
assert.ok(error.message.startsWith("Invalid emoji:"));
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
//#endregion
|
|
109
|
+
//# sourceMappingURL=emoji.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emoji.test.js","names":["emoji"],"sources":["../src/emoji.test.ts"],"sourcesContent":["// BotKit by Fedify: A framework for creating ActivityPub bots\n// Copyright (C) 2025 Hong Minhee <https://hongminhee.org/>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program. If not, see <https://www.gnu.org/licenses/>.\nimport assert from \"node:assert\";\nimport { test } from \"node:test\";\nimport { emoji, isEmoji } from \"./emoji.ts\";\n\ntest(\"isEmoji() with valid emojis\", () => {\n const validEmojis = [\n \"😀\", // simple emoji\n \"👍\", // thumbs up\n \"🚀\", // rocket\n \"🏳️🌈\", // pride flag (complex emoji with ZWJ sequence)\n \"👨👩👧👦\", // family (complex emoji with multiple ZWJ sequences)\n \"👩🏽🔬\", // woman scientist with medium skin tone\n \"🧘🏻♀️\", // woman in lotus position with light skin tone\n \"🤦♂️\", // man facepalming\n \"🇯🇵\", // flag\n ];\n\n for (const emoji of validEmojis) {\n assert.ok(\n isEmoji(emoji),\n `Expected '${emoji}' to be recognized as an emoji`,\n );\n }\n});\n\ntest(\"isEmoji() with invalid inputs\", () => {\n const invalidInputs = [\n // Multiple emojis\n \"😀😀\",\n \"👍🏻👎🏻\",\n // Regular text\n \"hello\",\n \"a\",\n // Mixed content\n \"hi😀\",\n \"👍awesome\",\n // Empty string\n \"\",\n // Non-string values\n 42,\n null,\n undefined,\n true,\n false,\n {},\n [],\n new Date(),\n ];\n\n for (const input of invalidInputs) {\n assert.strictEqual(\n isEmoji(input),\n false,\n `Expected '${input}' not to be recognized as an emoji`,\n );\n }\n});\n\ntest(\"isEmoji() with additional edge cases\", () => {\n const edgeCaseEmojis = [\n \"5️⃣\", // key cap sequence\n \"❤️\", // emoji with presentation variation selector\n \"☺️\", // older emoji with variation selector\n \"👩🦰\", // woman with red hair (hair modifier)\n \"🏊♀️\", // woman swimming (gender modifier)\n \"🧙♂️\", // man mage (gender modifier)\n \"🔢\", // input numbers symbol (legacy input emoji)\n \"↔️\", // arrow with variation selector\n \"📧\", // e-mail symbol\n \"📱\", // mobile phone\n ];\n\n for (const emoji of edgeCaseEmojis) {\n assert.ok(\n isEmoji(emoji),\n `Expected '${emoji}' to be recognized as an emoji`,\n );\n }\n});\n\ntest(\"isEmoji() with tricky invalid inputs\", () => {\n const trickyInvalidInputs = [\n \" 😀\", // emoji with leading space\n \"😀 \", // emoji with trailing space\n \"\\u200B😀\", // emoji with zero-width space\n // Note: Single regional indicators like \"🇺\" are technically valid emojis\n // even though they're usually paired to form flags\n \"\\u{1F3F4}\\uE0067\\uE0062\", // incomplete tag sequence\n \"\\uFE0F\", // variation selector alone\n \"\\u200D\", // zero width joiner alone\n \"♀️♂️\", // gender symbols together (should be two separate graphemes)\n ];\n\n for (const input of trickyInvalidInputs) {\n assert.strictEqual(\n isEmoji(input),\n false,\n `Expected '${input}' not to be recognized as an emoji`,\n );\n }\n});\n\ntest(\"emoji() tagged template function with valid emojis\", () => {\n const validEmojis = [\n emoji`😀`, // simple emoji\n emoji`👍`, // thumbs up\n emoji`🚀`, // rocket\n emoji`🏳️🌈`, // pride flag\n emoji`👨👩👧👦`, // family\n emoji`👩🏽🔬`, // woman scientist with medium skin tone\n emoji`🧘🏻♀️`, // woman in lotus position\n emoji`🇯🇵`, // flag\n ];\n\n for (const emojiValue of validEmojis) {\n assert.ok(isEmoji(emojiValue));\n }\n});\n\ntest(\"emoji() tagged template function with interpolation\", () => {\n const rocket = \"🚀\";\n const result = emoji`${rocket}`;\n assert.ok(isEmoji(result));\n assert.strictEqual(result, \"🚀\");\n});\n\ntest(\"emoji() throws with invalid inputs\", () => {\n const invalidInputs = [\n () => emoji`😀😀`, // multiple emojis\n () => emoji`hi😀`, // mixed content\n () => emoji`👍awesome`, // mixed content\n () => emoji` 😀`, // emoji with leading space\n () => emoji`😀 `, // emoji with trailing space\n () => emoji``, // empty string\n ];\n\n for (const fn of invalidInputs) {\n try {\n fn();\n assert.fail(\"Expected function to throw TypeError\");\n } catch (error) {\n assert.ok(error instanceof TypeError);\n assert.ok(error.message.startsWith(\"Invalid emoji:\"));\n }\n }\n});\n"],"mappings":";;;;;;;;;AAmBA,KAAK,+BAA+B,MAAM;CACxC,MAAM,cAAc;EAClB;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACD;AAED,MAAK,MAAMA,WAAS,YAClB,QAAO,GACL,QAAQA,QAAM,GACb,YAAYA,QAAM,gCACpB;AAEJ,EAAC;AAEF,KAAK,iCAAiC,MAAM;CAC1C,MAAM,gBAAgB;EAEpB;EACA;EAEA;EACA;EAEA;EACA;EAEA;EAEA;EACA;;EAEA;EACA;EACA,CAAE;EACF,CAAE;kBACF,IAAI;CACL;AAED,MAAK,MAAM,SAAS,cAClB,QAAO,YACL,QAAQ,MAAM,EACd,QACC,YAAY,MAAM,oCACpB;AAEJ,EAAC;AAEF,KAAK,wCAAwC,MAAM;CACjD,MAAM,iBAAiB;EACrB;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACD;AAED,MAAK,MAAMA,WAAS,eAClB,QAAO,GACL,QAAQA,QAAM,GACb,YAAYA,QAAM,gCACpB;AAEJ,EAAC;AAEF,KAAK,wCAAwC,MAAM;CACjD,MAAM,sBAAsB;EAC1B;EACA;EACA;EAGA;EACA;EACA;EACA;CACD;AAED,MAAK,MAAM,SAAS,oBAClB,QAAO,YACL,QAAQ,MAAM,EACd,QACC,YAAY,MAAM,oCACpB;AAEJ,EAAC;AAEF,KAAK,sDAAsD,MAAM;CAC/D,MAAM,cAAc;EAClB,MAAM;EACN,MAAM;EACN,MAAM;EACN,MAAM;EACN,MAAM;EACN,MAAM;EACN,MAAM;EACN,MAAM;CACP;AAED,MAAK,MAAM,cAAc,YACvB,QAAO,GAAG,QAAQ,WAAW,CAAC;AAEjC,EAAC;AAEF,KAAK,uDAAuD,MAAM;CAChE,MAAM,SAAS;CACf,MAAM,SAAS,MAAM,EAAE,OAAO;AAC9B,QAAO,GAAG,QAAQ,OAAO,CAAC;AAC1B,QAAO,YAAY,QAAQ,KAAK;AACjC,EAAC;AAEF,KAAK,sCAAsC,MAAM;CAC/C,MAAM,gBAAgB;EACpB,MAAM,MAAM;EACZ,MAAM,MAAM;EACZ,MAAM,MAAM;EACZ,MAAM,MAAM;EACZ,MAAM,MAAM;EACZ,MAAM,MAAM;CACb;AAED,MAAK,MAAM,MAAM,cACf,KAAI;AACF,MAAI;AACJ,SAAO,KAAK,uCAAuC;CACpD,SAAQ,OAAO;AACd,SAAO,GAAG,iBAAiB,UAAU;AACrC,SAAO,GAAG,MAAM,QAAQ,WAAW,iBAAiB,CAAC;CACtD;AAEJ,EAAC"}
|
package/dist/events.d.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Temporal, toTemporalInstant } from "@js-temporal/polyfill";
|
|
2
|
+
Date.prototype.toTemporalInstant = toTemporalInstant;
|
|
3
|
+
import { Like, Reaction } from "./reaction.js";
|
|
4
|
+
import { Message, MessageClass, SharedMessage } from "./message.js";
|
|
5
|
+
import { Session } from "./session.js";
|
|
6
|
+
import { FollowRequest } from "./follow.js";
|
|
7
|
+
import { Actor } from "@fedify/fedify/vocab";
|
|
8
|
+
|
|
9
|
+
//#region src/events.d.ts
|
|
10
|
+
/**
|
|
11
|
+
* An event handler for a follow request to the bot.
|
|
12
|
+
* @typeParam TContextData The type of the context data.
|
|
13
|
+
* @param session The session of the bot.
|
|
14
|
+
* @param followRequest The follow request.
|
|
15
|
+
*/
|
|
16
|
+
type FollowEventHandler<TContextData> = (session: Session<TContextData>, followRequest: FollowRequest) => void | Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* An event handler for an unfollow event from the bot.
|
|
19
|
+
* @typeParam TContextData The type of the context data.
|
|
20
|
+
* @param session The session of the bot.
|
|
21
|
+
* @param follower The actor who unfollowed the bot.
|
|
22
|
+
*/
|
|
23
|
+
type UnfollowEventHandler<TContextData> = (session: Session<TContextData>, follower: Actor) => void | Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* An event handler invoked when a follow request the bot sent is accepted.
|
|
26
|
+
* @typeParam TContextData The type of the context data.
|
|
27
|
+
* @param session The session of the bot.
|
|
28
|
+
* @param accepter The actor who accepted the follow request.
|
|
29
|
+
*/
|
|
30
|
+
type AcceptEventHandler<TContextData> = (session: Session<TContextData>, accepter: Actor) => void | Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* An event handler invoked when a follow request the bot sent is rejected.
|
|
33
|
+
* @typeParam TContextData The type of the context data.
|
|
34
|
+
* @param session The session of the bot.
|
|
35
|
+
* @param rejecter The actor who rejected the follow request.
|
|
36
|
+
*/
|
|
37
|
+
type RejectEventHandler<TContextData> = (session: Session<TContextData>, rejecter: Actor) => void | Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* An event handler for a message mentioned to the bot.
|
|
40
|
+
* @typeParam TContextData The type of the context data.
|
|
41
|
+
* @param session The session of the bot.
|
|
42
|
+
* @param message The mentioned message.
|
|
43
|
+
*/
|
|
44
|
+
type MentionEventHandler<TContextData> = (session: Session<TContextData>, message: Message<MessageClass, TContextData>) => void | Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* An event handler for a reply to the bot.
|
|
47
|
+
* @typeParam TContextData The type of the context data.
|
|
48
|
+
* @param session The session of the bot.
|
|
49
|
+
* @param reply The reply message.
|
|
50
|
+
*/
|
|
51
|
+
type ReplyEventHandler<TContextData> = (session: Session<TContextData>, reply: Message<MessageClass, TContextData>) => void | Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* An event handler for a quote of the bot's message.
|
|
54
|
+
* @typeParam TContextData The type of the context data.
|
|
55
|
+
* @param session The session of the bot.
|
|
56
|
+
* @param quote The message which quotes the bot's message.
|
|
57
|
+
* @since 0.2.0
|
|
58
|
+
*/
|
|
59
|
+
type QuoteEventHandler<TContextData> = (session: Session<TContextData>, quote: Message<MessageClass, TContextData>) => void | Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* An event handler for a message shown to the bot's timeline. To listen to
|
|
62
|
+
* this event, your bot needs to follow others first.
|
|
63
|
+
* @typeParam TContextData The type of the context data.
|
|
64
|
+
* @param session The session of the bot.
|
|
65
|
+
* @param message The message shown to the bot's timeline.
|
|
66
|
+
*/
|
|
67
|
+
type MessageEventHandler<TContextData> = (session: Session<TContextData>, message: Message<MessageClass, TContextData>) => void | Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* An event handler for a message shared by the bot. To listen to this event,
|
|
70
|
+
* your bot needs to follow others first.
|
|
71
|
+
* @typeParam TContextData The type of the context data.
|
|
72
|
+
* @param session The session of the bot.
|
|
73
|
+
* @param message The shared message to the bot's timeline.
|
|
74
|
+
*/
|
|
75
|
+
type SharedMessageEventHandler<TContextData> = (session: Session<TContextData>, message: SharedMessage<MessageClass, TContextData>) => void | Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* An event handler for a like of a message.
|
|
78
|
+
* @typeParam TContextData The type of the context data.
|
|
79
|
+
* @param session The session of the bot.
|
|
80
|
+
* @param like The like activity of the message.
|
|
81
|
+
*/
|
|
82
|
+
type LikeEventHandler<TContextData> = (session: Session<TContextData>, like: Like<TContextData>) => void | Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* An event handler for undoing a like of a message.
|
|
85
|
+
* @typeParam TContextData The type of the context data.
|
|
86
|
+
* @param session The session of the bot.
|
|
87
|
+
* @param like The like activity which is undone.
|
|
88
|
+
*/
|
|
89
|
+
type UnlikeEventHandler<TContextData> = (session: Session<TContextData>, like: Like<TContextData>) => void | Promise<void>;
|
|
90
|
+
/**
|
|
91
|
+
* An event handler for an emoji reaction to a message.
|
|
92
|
+
* @typeParam TContextData The type of the context data.
|
|
93
|
+
* @param session The session of the bot.
|
|
94
|
+
* @param reaction The emoji reaction to the message.
|
|
95
|
+
* @since 0.2.0
|
|
96
|
+
*/
|
|
97
|
+
type ReactionEventHandler<TContextData> = (session: Session<TContextData>, reaction: Reaction<TContextData>) => void | Promise<void>;
|
|
98
|
+
/**
|
|
99
|
+
* An event handler for undoing an emoji reaction to a message.
|
|
100
|
+
* @typeParam TContextData The type of the context data.
|
|
101
|
+
* @param session The session of the bot.
|
|
102
|
+
* @param reaction The emoji reaction to the message which is undone.
|
|
103
|
+
* @since 0.2.0
|
|
104
|
+
*/
|
|
105
|
+
type UndoneReactionEventHandler<TContextData> = (session: Session<TContextData>, reaction: Reaction<TContextData>) => void | Promise<void>;
|
|
106
|
+
//# sourceMappingURL=events.d.ts.map
|
|
107
|
+
|
|
108
|
+
//#endregion
|
|
109
|
+
export { AcceptEventHandler, FollowEventHandler, LikeEventHandler, MentionEventHandler, MessageEventHandler, QuoteEventHandler, ReactionEventHandler, RejectEventHandler, ReplyEventHandler, SharedMessageEventHandler, UndoneReactionEventHandler, UnfollowEventHandler, UnlikeEventHandler };
|
|
110
|
+
//# sourceMappingURL=events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.d.ts","names":[],"sources":["../src/events.ts"],"sourcesContent":[],"mappings":"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"}
|
package/dist/events.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Temporal, toTemporalInstant } from "@js-temporal/polyfill";
|
|
2
|
+
Date.prototype.toTemporalInstant = toTemporalInstant;
|
|
3
|
+
import { FollowRequest } from "./follow.js";
|
|
4
|
+
import { SessionImpl } from "./session-impl.js";
|
|
5
|
+
import { Actor, Follow } from "@fedify/fedify/vocab";
|
|
6
|
+
|
|
7
|
+
//#region src/follow-impl.d.ts
|
|
8
|
+
declare class FollowRequestImpl<TContextData> implements FollowRequest {
|
|
9
|
+
#private;
|
|
10
|
+
readonly session: SessionImpl<TContextData>;
|
|
11
|
+
readonly id: URL;
|
|
12
|
+
readonly raw: Follow;
|
|
13
|
+
readonly follower: Actor;
|
|
14
|
+
get state(): "pending" | "accepted" | "rejected";
|
|
15
|
+
constructor(session: SessionImpl<TContextData>, raw: Follow, follower: Actor);
|
|
16
|
+
accept(): Promise<void>;
|
|
17
|
+
reject(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=follow-impl.d.ts.map
|
|
20
|
+
|
|
21
|
+
//#endregion
|
|
22
|
+
export { FollowRequestImpl };
|
|
23
|
+
//# sourceMappingURL=follow-impl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"follow-impl.d.ts","names":[],"sources":["../src/follow-impl.ts"],"sourcesContent":[],"mappings":";;;;;;;cAmBa,2CAA2C;;oBACpC,YAAY;eACjB;gBACC;qBACK;EAJR,IAAA,KAAA,CAAA,CAAA,EAAA,SAAiB,GAAA,UAAA,GAAA,UAAA;EAAA,WAAA,CAAA,OAAA,EAYjB,WAZiB,CAYL,YAZK,CAAA,EAAA,GAAA,EAarB,MAbqB,EAAA,QAAA,EAchB,KAdgB;EAAA,MACE,CAAA,CAAA,EA2Bd,OA3Bc,CAAA,IAAA,CAAA;EAAY,MAAxB,CAAA,CAAA,EA8CF,OA9CE,CAAA,IAAA,CAAA"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
|
|
2
|
+
import { Temporal, toTemporalInstant } from "@js-temporal/polyfill";
|
|
3
|
+
Date.prototype.toTemporalInstant = toTemporalInstant;
|
|
4
|
+
|
|
5
|
+
import { Accept, Reject } from "@fedify/fedify/vocab";
|
|
6
|
+
|
|
7
|
+
//#region src/follow-impl.ts
|
|
8
|
+
var FollowRequestImpl = class {
|
|
9
|
+
session;
|
|
10
|
+
id;
|
|
11
|
+
raw;
|
|
12
|
+
follower;
|
|
13
|
+
#state;
|
|
14
|
+
get state() {
|
|
15
|
+
return this.#state;
|
|
16
|
+
}
|
|
17
|
+
constructor(session, raw, follower) {
|
|
18
|
+
if (raw.id == null) throw new TypeError("The follow request ID is missing.");
|
|
19
|
+
else if (follower.id == null) throw new TypeError("The follower ID is missing.");
|
|
20
|
+
this.session = session;
|
|
21
|
+
this.id = raw.id;
|
|
22
|
+
this.raw = raw;
|
|
23
|
+
this.follower = follower;
|
|
24
|
+
this.#state = "pending";
|
|
25
|
+
}
|
|
26
|
+
async accept() {
|
|
27
|
+
if (this.#state !== "pending") throw new TypeError("The follow request is not pending.");
|
|
28
|
+
await this.session.context.sendActivity(this.session.bot, this.follower, new Accept({
|
|
29
|
+
id: new URL(`/#accept/${this.id.href}`, this.session.actorId),
|
|
30
|
+
actor: this.session.actorId,
|
|
31
|
+
to: this.follower.id,
|
|
32
|
+
object: this.raw
|
|
33
|
+
}), { excludeBaseUris: [new URL(this.session.context.origin)] });
|
|
34
|
+
await this.session.bot.repository.addFollower(this.id, this.follower);
|
|
35
|
+
this.#state = "accepted";
|
|
36
|
+
}
|
|
37
|
+
async reject() {
|
|
38
|
+
if (this.#state !== "pending") throw new TypeError("The follow request is not pending.");
|
|
39
|
+
await this.session.context.sendActivity(this.session.bot, this.follower, new Reject({
|
|
40
|
+
id: new URL(`/#accept/${this.id.href}`, this.session.actorId),
|
|
41
|
+
actor: this.session.actorId,
|
|
42
|
+
to: this.follower.id,
|
|
43
|
+
object: this.raw
|
|
44
|
+
}), { excludeBaseUris: [new URL(this.session.context.origin)] });
|
|
45
|
+
this.#state = "rejected";
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
//#endregion
|
|
50
|
+
export { FollowRequestImpl };
|
|
51
|
+
//# sourceMappingURL=follow-impl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"follow-impl.js","names":["#state","session: SessionImpl<TContextData>","raw: Follow","follower: Actor"],"sources":["../src/follow-impl.ts"],"sourcesContent":["// BotKit by Fedify: A framework for creating ActivityPub bots\n// Copyright (C) 2025 Hong Minhee <https://hongminhee.org/>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program. If not, see <https://www.gnu.org/licenses/>.\nimport { Accept, type Actor, type Follow, Reject } from \"@fedify/fedify/vocab\";\nimport type { FollowRequest } from \"./follow.ts\";\nimport type { SessionImpl } from \"./session-impl.ts\";\n\nexport class FollowRequestImpl<TContextData> implements FollowRequest {\n readonly session: SessionImpl<TContextData>;\n readonly id: URL;\n readonly raw: Follow;\n readonly follower: Actor;\n #state: \"pending\" | \"accepted\" | \"rejected\";\n\n get state(): \"pending\" | \"accepted\" | \"rejected\" {\n return this.#state;\n }\n\n constructor(\n session: SessionImpl<TContextData>,\n raw: Follow,\n follower: Actor,\n ) {\n if (raw.id == null) {\n throw new TypeError(\"The follow request ID is missing.\");\n } else if (follower.id == null) {\n throw new TypeError(\"The follower ID is missing.\");\n }\n this.session = session;\n this.id = raw.id;\n this.raw = raw;\n this.follower = follower;\n this.#state = \"pending\";\n }\n\n async accept(): Promise<void> {\n if (this.#state !== \"pending\") {\n throw new TypeError(\"The follow request is not pending.\");\n }\n await this.session.context.sendActivity(\n this.session.bot,\n this.follower,\n new Accept({\n id: new URL(`/#accept/${this.id.href}`, this.session.actorId),\n actor: this.session.actorId,\n to: this.follower.id,\n object: this.raw,\n }),\n { excludeBaseUris: [new URL(this.session.context.origin)] },\n );\n await this.session.bot.repository.addFollower(this.id, this.follower);\n this.#state = \"accepted\";\n }\n\n async reject(): Promise<void> {\n if (this.#state !== \"pending\") {\n throw new TypeError(\"The follow request is not pending.\");\n }\n await this.session.context.sendActivity(\n this.session.bot,\n this.follower,\n new Reject({\n id: new URL(`/#accept/${this.id.href}`, this.session.actorId),\n actor: this.session.actorId,\n to: this.follower.id,\n object: this.raw,\n }),\n { excludeBaseUris: [new URL(this.session.context.origin)] },\n );\n this.#state = \"rejected\";\n }\n}\n"],"mappings":";;;;;;;AAmBA,IAAa,oBAAb,MAAsE;CACpE,AAAS;CACT,AAAS;CACT,AAAS;CACT,AAAS;CACT;CAEA,IAAI,QAA6C;AAC/C,SAAO,KAAKA;CACb;CAED,YACEC,SACAC,KACAC,UACA;AACA,MAAI,IAAI,MAAM,KACZ,OAAM,IAAI,UAAU;WACX,SAAS,MAAM,KACxB,OAAM,IAAI,UAAU;AAEtB,OAAK,UAAU;AACf,OAAK,KAAK,IAAI;AACd,OAAK,MAAM;AACX,OAAK,WAAW;AAChB,OAAKH,SAAS;CACf;CAED,MAAM,SAAwB;AAC5B,MAAI,KAAKA,WAAW,UAClB,OAAM,IAAI,UAAU;AAEtB,QAAM,KAAK,QAAQ,QAAQ,aACzB,KAAK,QAAQ,KACb,KAAK,UACL,IAAI,OAAO;GACT,IAAI,IAAI,KAAK,WAAW,KAAK,GAAG,KAAK,GAAG,KAAK,QAAQ;GACrD,OAAO,KAAK,QAAQ;GACpB,IAAI,KAAK,SAAS;GAClB,QAAQ,KAAK;EACd,IACD,EAAE,iBAAiB,CAAC,IAAI,IAAI,KAAK,QAAQ,QAAQ,OAAQ,EAAE,EAC5D;AACD,QAAM,KAAK,QAAQ,IAAI,WAAW,YAAY,KAAK,IAAI,KAAK,SAAS;AACrE,OAAKA,SAAS;CACf;CAED,MAAM,SAAwB;AAC5B,MAAI,KAAKA,WAAW,UAClB,OAAM,IAAI,UAAU;AAEtB,QAAM,KAAK,QAAQ,QAAQ,aACzB,KAAK,QAAQ,KACb,KAAK,UACL,IAAI,OAAO;GACT,IAAI,IAAI,KAAK,WAAW,KAAK,GAAG,KAAK,GAAG,KAAK,QAAQ;GACrD,OAAO,KAAK,QAAQ;GACpB,IAAI,KAAK,SAAS;GAClB,QAAQ,KAAK;EACd,IACD,EAAE,iBAAiB,CAAC,IAAI,IAAI,KAAK,QAAQ,QAAQ,OAAQ,EAAE,EAC5D;AACD,OAAKA,SAAS;CACf;AACF"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
|
|
2
|
+
import { Temporal, toTemporalInstant } from "@js-temporal/polyfill";
|
|
3
|
+
Date.prototype.toTemporalInstant = toTemporalInstant;
|
|
4
|
+
|
|
5
|
+
import { FollowRequestImpl } from "./follow-impl.js";
|
|
6
|
+
import { MemoryRepository } from "./repository.js";
|
|
7
|
+
import { SessionImpl } from "./session-impl.js";
|
|
8
|
+
import { BotImpl } from "./bot-impl.js";
|
|
9
|
+
import { createMockContext } from "./session-impl.test.js";
|
|
10
|
+
import { MemoryKvStore } from "@fedify/fedify/federation";
|
|
11
|
+
import { Accept, Follow, Reject } from "@fedify/fedify/vocab";
|
|
12
|
+
import assert from "node:assert";
|
|
13
|
+
import { test } from "node:test";
|
|
14
|
+
import { Person as Person$1 } from "@fedify/fedify";
|
|
15
|
+
|
|
16
|
+
//#region src/follow-impl.test.ts
|
|
17
|
+
test("new FollowRequestImpl()", () => {
|
|
18
|
+
const bot = new BotImpl({
|
|
19
|
+
kv: new MemoryKvStore(),
|
|
20
|
+
username: "bot"
|
|
21
|
+
});
|
|
22
|
+
const ctx = createMockContext(bot, "https://example.com");
|
|
23
|
+
const session = new SessionImpl(bot, ctx);
|
|
24
|
+
const follower = new Person$1({
|
|
25
|
+
id: new URL("https://example.com/ap/actor/john"),
|
|
26
|
+
preferredUsername: "john"
|
|
27
|
+
});
|
|
28
|
+
const follow = new Follow({
|
|
29
|
+
id: new URL("https://example.com/ap/follow/1"),
|
|
30
|
+
actor: follower,
|
|
31
|
+
object: session.actorId
|
|
32
|
+
});
|
|
33
|
+
const followRequest = new FollowRequestImpl(session, follow, follower);
|
|
34
|
+
assert.deepStrictEqual(followRequest.session, session);
|
|
35
|
+
assert.deepStrictEqual(followRequest.id, follow.id);
|
|
36
|
+
assert.deepStrictEqual(followRequest.raw, follow);
|
|
37
|
+
assert.deepStrictEqual(followRequest.follower, follower);
|
|
38
|
+
assert.deepStrictEqual(followRequest.state, "pending");
|
|
39
|
+
});
|
|
40
|
+
test("FollowRequestImpl.accept()", async () => {
|
|
41
|
+
const repository = new MemoryRepository();
|
|
42
|
+
const bot = new BotImpl({
|
|
43
|
+
kv: new MemoryKvStore(),
|
|
44
|
+
repository,
|
|
45
|
+
username: "bot"
|
|
46
|
+
});
|
|
47
|
+
const ctx = createMockContext(bot, "https://example.com");
|
|
48
|
+
const session = new SessionImpl(bot, ctx);
|
|
49
|
+
const follower = new Person$1({
|
|
50
|
+
id: new URL("https://example.com/ap/actor/john"),
|
|
51
|
+
preferredUsername: "john"
|
|
52
|
+
});
|
|
53
|
+
const follow = new Follow({
|
|
54
|
+
id: new URL("https://example.com/ap/follow/1"),
|
|
55
|
+
actor: follower,
|
|
56
|
+
object: session.actorId
|
|
57
|
+
});
|
|
58
|
+
const followRequest = new FollowRequestImpl(session, follow, follower);
|
|
59
|
+
await followRequest.accept();
|
|
60
|
+
assert.deepStrictEqual(followRequest.state, "accepted");
|
|
61
|
+
assert.ok(await repository.hasFollower(new URL("https://example.com/ap/actor/john")));
|
|
62
|
+
const [storedFollower] = await Array.fromAsync(repository.getFollowers());
|
|
63
|
+
assert.ok(storedFollower != null);
|
|
64
|
+
assert.deepStrictEqual(storedFollower.id, follower.id);
|
|
65
|
+
assert.deepStrictEqual(storedFollower.preferredUsername, follower.preferredUsername);
|
|
66
|
+
assert.deepStrictEqual(ctx.sentActivities.length, 1);
|
|
67
|
+
const { recipients, activity } = ctx.sentActivities[0];
|
|
68
|
+
assert.deepStrictEqual(recipients, [follower]);
|
|
69
|
+
assert.ok(activity instanceof Accept);
|
|
70
|
+
assert.deepStrictEqual(activity.actorId, session.actorId);
|
|
71
|
+
assert.deepStrictEqual(activity.toId, follower.id);
|
|
72
|
+
assert.deepStrictEqual(activity.objectId, follow.id);
|
|
73
|
+
assert.rejects(() => followRequest.accept(), TypeError, "The follow request is not pending.");
|
|
74
|
+
assert.rejects(() => followRequest.reject(), TypeError, "The follow request is not pending.");
|
|
75
|
+
});
|
|
76
|
+
test("FollowRequestImpl.reject()", async () => {
|
|
77
|
+
const repository = new MemoryRepository();
|
|
78
|
+
const bot = new BotImpl({
|
|
79
|
+
kv: new MemoryKvStore(),
|
|
80
|
+
repository,
|
|
81
|
+
username: "bot"
|
|
82
|
+
});
|
|
83
|
+
const ctx = createMockContext(bot, "https://example.com");
|
|
84
|
+
const session = new SessionImpl(bot, ctx);
|
|
85
|
+
const follower = new Person$1({
|
|
86
|
+
id: new URL("https://example.com/ap/actor/john"),
|
|
87
|
+
preferredUsername: "john"
|
|
88
|
+
});
|
|
89
|
+
const follow = new Follow({
|
|
90
|
+
id: new URL("https://example.com/ap/follow/1"),
|
|
91
|
+
actor: follower,
|
|
92
|
+
object: session.actorId
|
|
93
|
+
});
|
|
94
|
+
const followRequest = new FollowRequestImpl(session, follow, follower);
|
|
95
|
+
await followRequest.reject();
|
|
96
|
+
assert.deepStrictEqual(followRequest.state, "rejected");
|
|
97
|
+
assert.deepStrictEqual(await repository.hasFollower(new URL("https://example.com/ap/actor/john")), false);
|
|
98
|
+
assert.deepStrictEqual(ctx.sentActivities.length, 1);
|
|
99
|
+
const { recipients, activity } = ctx.sentActivities[0];
|
|
100
|
+
assert.deepStrictEqual(recipients, [follower]);
|
|
101
|
+
assert.ok(activity instanceof Reject);
|
|
102
|
+
assert.deepStrictEqual(activity.actorId, session.actorId);
|
|
103
|
+
assert.deepStrictEqual(activity.toId, follower.id);
|
|
104
|
+
assert.deepStrictEqual(activity.objectId, follow.id);
|
|
105
|
+
assert.rejects(() => followRequest.accept(), TypeError, "The follow request is not pending.");
|
|
106
|
+
assert.rejects(() => followRequest.reject(), TypeError, "The follow request is not pending.");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
//#endregion
|
|
110
|
+
//# sourceMappingURL=follow-impl.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"follow-impl.test.js","names":["Person"],"sources":["../src/follow-impl.test.ts"],"sourcesContent":["// BotKit by Fedify: A framework for creating ActivityPub bots\n// Copyright (C) 2025 Hong Minhee <https://hongminhee.org/>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program. If not, see <https://www.gnu.org/licenses/>.\nimport { Person } from \"@fedify/fedify\";\nimport { MemoryKvStore } from \"@fedify/fedify/federation\";\nimport { Accept, Follow, Reject } from \"@fedify/fedify/vocab\";\nimport assert from \"node:assert\";\nimport { test } from \"node:test\";\nimport { BotImpl } from \"./bot-impl.ts\";\nimport { FollowRequestImpl } from \"./follow-impl.ts\";\nimport { MemoryRepository } from \"./repository.ts\";\nimport { createMockContext } from \"./session-impl.test.ts\";\nimport { SessionImpl } from \"./session-impl.ts\";\n\ntest(\"new FollowRequestImpl()\", () => {\n const bot = new BotImpl<void>({ kv: new MemoryKvStore(), username: \"bot\" });\n const ctx = createMockContext(bot, \"https://example.com\");\n const session = new SessionImpl(bot, ctx);\n\n const follower = new Person({\n id: new URL(\"https://example.com/ap/actor/john\"),\n preferredUsername: \"john\",\n });\n const follow = new Follow({\n id: new URL(\"https://example.com/ap/follow/1\"),\n actor: follower,\n object: session.actorId,\n });\n const followRequest = new FollowRequestImpl(session, follow, follower);\n assert.deepStrictEqual(followRequest.session, session);\n assert.deepStrictEqual(followRequest.id, follow.id);\n assert.deepStrictEqual(followRequest.raw, follow);\n assert.deepStrictEqual(followRequest.follower, follower);\n assert.deepStrictEqual(followRequest.state, \"pending\");\n});\n\ntest(\"FollowRequestImpl.accept()\", async () => {\n const repository = new MemoryRepository();\n const bot = new BotImpl<void>({\n kv: new MemoryKvStore(),\n repository,\n username: \"bot\",\n });\n const ctx = createMockContext(bot, \"https://example.com\");\n const session = new SessionImpl(bot, ctx);\n\n const follower = new Person({\n id: new URL(\"https://example.com/ap/actor/john\"),\n preferredUsername: \"john\",\n });\n const follow = new Follow({\n id: new URL(\"https://example.com/ap/follow/1\"),\n actor: follower,\n object: session.actorId,\n });\n const followRequest = new FollowRequestImpl(session, follow, follower);\n await followRequest.accept();\n assert.deepStrictEqual(followRequest.state, \"accepted\");\n assert.ok(\n await repository.hasFollower(new URL(\"https://example.com/ap/actor/john\")),\n );\n const [storedFollower] = await Array.fromAsync(repository.getFollowers());\n assert.ok(storedFollower != null);\n assert.deepStrictEqual(storedFollower.id, follower.id);\n assert.deepStrictEqual(\n storedFollower.preferredUsername,\n follower.preferredUsername,\n );\n assert.deepStrictEqual(ctx.sentActivities.length, 1);\n const { recipients, activity } = ctx.sentActivities[0];\n assert.deepStrictEqual(recipients, [follower]);\n assert.ok(activity instanceof Accept);\n assert.deepStrictEqual(activity.actorId, session.actorId);\n assert.deepStrictEqual(activity.toId, follower.id);\n assert.deepStrictEqual(activity.objectId, follow.id);\n\n assert.rejects(\n () => followRequest.accept(),\n TypeError,\n \"The follow request is not pending.\",\n );\n assert.rejects(\n () => followRequest.reject(),\n TypeError,\n \"The follow request is not pending.\",\n );\n});\n\ntest(\"FollowRequestImpl.reject()\", async () => {\n const repository = new MemoryRepository();\n const bot = new BotImpl<void>({\n kv: new MemoryKvStore(),\n repository,\n username: \"bot\",\n });\n const ctx = createMockContext(bot, \"https://example.com\");\n const session = new SessionImpl(bot, ctx);\n\n const follower = new Person({\n id: new URL(\"https://example.com/ap/actor/john\"),\n preferredUsername: \"john\",\n });\n const follow = new Follow({\n id: new URL(\"https://example.com/ap/follow/1\"),\n actor: follower,\n object: session.actorId,\n });\n const followRequest = new FollowRequestImpl(session, follow, follower);\n await followRequest.reject();\n assert.deepStrictEqual(followRequest.state, \"rejected\");\n assert.deepStrictEqual(\n await repository.hasFollower(new URL(\"https://example.com/ap/actor/john\")),\n false,\n );\n assert.deepStrictEqual(ctx.sentActivities.length, 1);\n const { recipients, activity } = ctx.sentActivities[0];\n assert.deepStrictEqual(recipients, [follower]);\n assert.ok(activity instanceof Reject);\n assert.deepStrictEqual(activity.actorId, session.actorId);\n assert.deepStrictEqual(activity.toId, follower.id);\n assert.deepStrictEqual(activity.objectId, follow.id);\n\n assert.rejects(\n () => followRequest.accept(),\n TypeError,\n \"The follow request is not pending.\",\n );\n assert.rejects(\n () => followRequest.reject(),\n TypeError,\n \"The follow request is not pending.\",\n );\n});\n"],"mappings":"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"}
|