@overpod/mcp-telegram 1.25.0 → 1.26.1
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/CHANGELOG.md +35 -0
- package/README.md +35 -5
- package/dist/index.js +4 -1
- package/dist/rate-limiter.d.ts +9 -3
- package/dist/rate-limiter.js +23 -16
- package/dist/telegram-client.d.ts +134 -18
- package/dist/telegram-client.js +601 -136
- package/dist/telegram-helpers.d.ts +470 -0
- package/dist/telegram-helpers.js +870 -0
- package/dist/tools/account.js +22 -6
- package/dist/tools/boosts.d.ts +3 -0
- package/dist/tools/boosts.js +65 -0
- package/dist/tools/chats.js +155 -5
- package/dist/tools/contacts.js +3 -3
- package/dist/tools/extras.js +3 -3
- package/dist/tools/group-calls.d.ts +4 -0
- package/dist/tools/group-calls.js +77 -0
- package/dist/tools/index.js +10 -0
- package/dist/tools/messages.js +203 -11
- package/dist/tools/quick-replies.d.ts +4 -0
- package/dist/tools/quick-replies.js +58 -0
- package/dist/tools/reactions.js +45 -2
- package/dist/tools/shared.d.ts +3 -3
- package/dist/tools/shared.js +8 -7
- package/dist/tools/stars.d.ts +4 -0
- package/dist/tools/stars.js +71 -0
- package/dist/tools/stickers.js +5 -5
- package/dist/tools/stories.d.ts +3 -0
- package/dist/tools/stories.js +107 -0
- package/package.json +1 -1
- package/dist/__tests__/admin-log.test.d.ts +0 -1
- package/dist/__tests__/admin-log.test.js +0 -41
- package/dist/__tests__/rate-limiter.test.d.ts +0 -1
- package/dist/__tests__/rate-limiter.test.js +0 -81
- package/dist/__tests__/reactions.test.d.ts +0 -1
- package/dist/__tests__/reactions.test.js +0 -23
- package/dist/__tests__/set-chat-permissions-merge.test.d.ts +0 -1
- package/dist/__tests__/set-chat-permissions-merge.test.js +0 -107
- package/dist/__tests__/tools/shared.test.d.ts +0 -1
- package/dist/__tests__/tools/shared.test.js +0 -110
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { fail, ok, READ_ONLY, requireConnection } from "./shared.js";
|
|
3
|
+
export function registerStoryTools(server, telegram) {
|
|
4
|
+
server.registerTool("telegram-get-all-stories", {
|
|
5
|
+
description: "Fetch active stories from contacts/channels the user follows. Pagination via 'next' + 'state' — pass the returned state back on the next call with next:true to load more. Use hidden:true to read stories from muted/archived peers. Returns compact story metadata (id, date, expireDate, caption, mediaType, counters) without raw media blobs.",
|
|
6
|
+
inputSchema: {
|
|
7
|
+
next: z.boolean().optional().describe("Load the next page (use with state from a prior response)"),
|
|
8
|
+
hidden: z.boolean().optional().describe("Fetch stories from hidden/archived peers instead of the main feed"),
|
|
9
|
+
state: z.string().optional().describe("Pagination state token returned by a previous call"),
|
|
10
|
+
},
|
|
11
|
+
annotations: READ_ONLY,
|
|
12
|
+
}, async ({ next, hidden, state }) => {
|
|
13
|
+
const err = await requireConnection(telegram);
|
|
14
|
+
if (err)
|
|
15
|
+
return fail(new Error(err));
|
|
16
|
+
if (next === true && !state) {
|
|
17
|
+
return fail(new Error("'state' is required when 'next' is true — use the state token from a prior telegram-get-all-stories response"));
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const result = await telegram.getAllStories({ next, hidden, state });
|
|
21
|
+
return ok(JSON.stringify(result));
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
return fail(e);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
server.registerTool("telegram-get-peer-stories", {
|
|
28
|
+
description: "Fetch currently active stories posted by a specific peer (user/channel). Returns compact story metadata (id, date, expireDate, caption, mediaType, counters) with media type className only — no raw media blobs. Use telegram-download-media with the story id if you need media bytes.",
|
|
29
|
+
inputSchema: {
|
|
30
|
+
chat: z
|
|
31
|
+
.string()
|
|
32
|
+
.describe("Peer to fetch stories from — user/channel id, @username, phone, or display name fragment"),
|
|
33
|
+
},
|
|
34
|
+
annotations: READ_ONLY,
|
|
35
|
+
}, async ({ chat }) => {
|
|
36
|
+
const err = await requireConnection(telegram);
|
|
37
|
+
if (err)
|
|
38
|
+
return fail(new Error(err));
|
|
39
|
+
try {
|
|
40
|
+
const result = await telegram.getPeerStories(chat);
|
|
41
|
+
if (result === null)
|
|
42
|
+
return ok("No active stories found for the specified peer");
|
|
43
|
+
return ok(JSON.stringify(result));
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
return fail(e);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
server.registerTool("telegram-get-stories-by-id", {
|
|
50
|
+
description: "Fetch specific stories from a peer by their numeric IDs. Useful for retrieving archived/pinned stories outside the active feed. Returns compact story metadata and optional pinnedToTop list. Pass up to ~100 ids per request.",
|
|
51
|
+
inputSchema: {
|
|
52
|
+
chat: z
|
|
53
|
+
.string()
|
|
54
|
+
.describe("Peer to fetch stories from — user/channel id, @username, phone, or display name fragment"),
|
|
55
|
+
ids: z.array(z.number().int().positive()).min(1).max(100).describe("Story IDs to fetch (1–100 per request)"),
|
|
56
|
+
},
|
|
57
|
+
annotations: READ_ONLY,
|
|
58
|
+
}, async ({ chat, ids }) => {
|
|
59
|
+
const err = await requireConnection(telegram);
|
|
60
|
+
if (err)
|
|
61
|
+
return fail(new Error(err));
|
|
62
|
+
try {
|
|
63
|
+
const result = await telegram.getStoriesById(chat, ids);
|
|
64
|
+
return ok(JSON.stringify(result));
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
return fail(e);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
server.registerTool("telegram-get-story-views", {
|
|
71
|
+
description: "List viewers of one of YOUR stories (stories.GetStoryViewsList). Returns per-viewer entries (user id, view date, their reaction emoji if any), plus totals (viewsCount, forwardsCount, reactionsCount) and nextOffset for pagination. Pass your own user id (numeric) or @username as the peer — this only works for stories you posted. Some accounts (non-Premium, old stories) may not get a full viewer list — a Premium hint is surfaced on typical errors.",
|
|
72
|
+
inputSchema: {
|
|
73
|
+
chat: z.string().describe("Peer owning the story — usually 'me' or your own user id/@username"),
|
|
74
|
+
storyId: z.number().int().positive().describe("Story ID to fetch viewers for"),
|
|
75
|
+
q: z.string().optional().describe("Filter viewers by name substring"),
|
|
76
|
+
justContacts: z.boolean().optional().describe("Return only contacts"),
|
|
77
|
+
reactionsFirst: z.boolean().optional().describe("Sort viewers who reacted first"),
|
|
78
|
+
forwardsFirst: z.boolean().optional().describe("Sort forwards/reposts first"),
|
|
79
|
+
offset: z.string().optional().describe("Pagination offset from a previous response's nextOffset"),
|
|
80
|
+
limit: z.number().int().min(1).max(100).optional().describe("Max viewers to return (default 50, max 100)"),
|
|
81
|
+
},
|
|
82
|
+
annotations: READ_ONLY,
|
|
83
|
+
}, async ({ chat, storyId, q, justContacts, reactionsFirst, forwardsFirst, offset, limit }) => {
|
|
84
|
+
const err = await requireConnection(telegram);
|
|
85
|
+
if (err)
|
|
86
|
+
return fail(new Error(err));
|
|
87
|
+
try {
|
|
88
|
+
const result = await telegram.getStoryViewsList(chat, {
|
|
89
|
+
id: storyId,
|
|
90
|
+
q,
|
|
91
|
+
justContacts,
|
|
92
|
+
reactionsFirst,
|
|
93
|
+
forwardsFirst,
|
|
94
|
+
offset,
|
|
95
|
+
limit,
|
|
96
|
+
});
|
|
97
|
+
return ok(JSON.stringify(result));
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
const msg = e.message ?? "";
|
|
101
|
+
if (/PREMIUM|PAYMENT_REQUIRED/i.test(msg)) {
|
|
102
|
+
return fail(new Error("story view stats may require Telegram Premium"));
|
|
103
|
+
}
|
|
104
|
+
return fail(e);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import { Api } from "telegram/tl/index.js";
|
|
4
|
-
import { describeAdminLogAction, describeAdminLogDetails } from "../telegram-client.js";
|
|
5
|
-
describe("describeAdminLogAction", () => {
|
|
6
|
-
it("converts ChangeTitle to snake_case", () => {
|
|
7
|
-
const action = new Api.ChannelAdminLogEventActionChangeTitle({ prevValue: "a", newValue: "b" });
|
|
8
|
-
assert.strictEqual(describeAdminLogAction(action), "change_title");
|
|
9
|
-
});
|
|
10
|
-
it("converts ParticipantJoin to snake_case", () => {
|
|
11
|
-
const action = new Api.ChannelAdminLogEventActionParticipantJoin();
|
|
12
|
-
assert.strictEqual(describeAdminLogAction(action), "participant_join");
|
|
13
|
-
});
|
|
14
|
-
it("handles ToggleSlowMode", () => {
|
|
15
|
-
const action = new Api.ChannelAdminLogEventActionToggleSlowMode({ prevValue: 0, newValue: 30 });
|
|
16
|
-
assert.strictEqual(describeAdminLogAction(action), "toggle_slow_mode");
|
|
17
|
-
});
|
|
18
|
-
it("handles ChangeHistoryTTL without splitting acronym", () => {
|
|
19
|
-
const action = new Api.ChannelAdminLogEventActionChangeHistoryTTL({ prevValue: 0, newValue: 86400 });
|
|
20
|
-
assert.strictEqual(describeAdminLogAction(action), "change_history_ttl");
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
describe("describeAdminLogDetails", () => {
|
|
24
|
-
const describeUser = (id) => `user_${id.toString()}`;
|
|
25
|
-
it("formats title change", () => {
|
|
26
|
-
const action = new Api.ChannelAdminLogEventActionChangeTitle({ prevValue: "old", newValue: "new" });
|
|
27
|
-
assert.strictEqual(describeAdminLogDetails(action, describeUser), '"old" → "new"');
|
|
28
|
-
});
|
|
29
|
-
it("formats username change", () => {
|
|
30
|
-
const action = new Api.ChannelAdminLogEventActionChangeUsername({ prevValue: "old", newValue: "new" });
|
|
31
|
-
assert.strictEqual(describeAdminLogDetails(action, describeUser), "@old → @new");
|
|
32
|
-
});
|
|
33
|
-
it("formats slow mode change", () => {
|
|
34
|
-
const action = new Api.ChannelAdminLogEventActionToggleSlowMode({ prevValue: 0, newValue: 30 });
|
|
35
|
-
assert.strictEqual(describeAdminLogDetails(action, describeUser), "0s → 30s");
|
|
36
|
-
});
|
|
37
|
-
it("returns empty string for unknown actions", () => {
|
|
38
|
-
const action = new Api.ChannelAdminLogEventActionParticipantJoin();
|
|
39
|
-
assert.strictEqual(describeAdminLogDetails(action, describeUser), "");
|
|
40
|
-
});
|
|
41
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import { RateLimiter } from "../rate-limiter.js";
|
|
4
|
-
describe("RateLimiter", () => {
|
|
5
|
-
it("should execute a function successfully", async () => {
|
|
6
|
-
const limiter = new RateLimiter({ maxRequestsPerSecond: 100 });
|
|
7
|
-
const result = await limiter.execute(async () => "success");
|
|
8
|
-
assert.strictEqual(result, "success");
|
|
9
|
-
});
|
|
10
|
-
it("should enforce rate limiting between requests", async () => {
|
|
11
|
-
const limiter = new RateLimiter({ maxRequestsPerSecond: 10 }); // 10 req/s = 100ms between requests
|
|
12
|
-
const start = Date.now();
|
|
13
|
-
await limiter.execute(async () => "first");
|
|
14
|
-
await limiter.execute(async () => "second");
|
|
15
|
-
const elapsed = Date.now() - start;
|
|
16
|
-
assert.ok(elapsed >= 90, `Expected at least 90ms, got ${elapsed}ms`);
|
|
17
|
-
});
|
|
18
|
-
it("should retry on FLOOD_WAIT error", async () => {
|
|
19
|
-
const limiter = new RateLimiter({ maxRetries: 2, maxRequestsPerSecond: 100 });
|
|
20
|
-
let attempts = 0;
|
|
21
|
-
const result = await limiter.execute(async () => {
|
|
22
|
-
attempts++;
|
|
23
|
-
if (attempts < 2) {
|
|
24
|
-
throw new Error("FLOOD_WAIT_1");
|
|
25
|
-
}
|
|
26
|
-
return "success after retry";
|
|
27
|
-
});
|
|
28
|
-
assert.strictEqual(result, "success after retry");
|
|
29
|
-
assert.strictEqual(attempts, 2);
|
|
30
|
-
});
|
|
31
|
-
it("should throw after max retries on FLOOD_WAIT", async () => {
|
|
32
|
-
const limiter = new RateLimiter({ maxRetries: 1, maxRequestsPerSecond: 100 });
|
|
33
|
-
await assert.rejects(async () => {
|
|
34
|
-
await limiter.execute(async () => {
|
|
35
|
-
throw new Error("FLOOD_WAIT_2");
|
|
36
|
-
});
|
|
37
|
-
}, {
|
|
38
|
-
message: /Rate limit exceeded after 1 retries/,
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
it("should retry on network errors with exponential backoff", async () => {
|
|
42
|
-
const limiter = new RateLimiter({
|
|
43
|
-
maxRetries: 2,
|
|
44
|
-
initialRetryDelay: 100,
|
|
45
|
-
maxRequestsPerSecond: 100,
|
|
46
|
-
});
|
|
47
|
-
let attempts = 0;
|
|
48
|
-
const result = await limiter.execute(async () => {
|
|
49
|
-
attempts++;
|
|
50
|
-
if (attempts < 2) {
|
|
51
|
-
throw new Error("TIMEOUT");
|
|
52
|
-
}
|
|
53
|
-
return "recovered";
|
|
54
|
-
});
|
|
55
|
-
assert.strictEqual(result, "recovered");
|
|
56
|
-
assert.strictEqual(attempts, 2);
|
|
57
|
-
});
|
|
58
|
-
it("should not retry on non-retryable errors", async () => {
|
|
59
|
-
const limiter = new RateLimiter({ maxRetries: 3, maxRequestsPerSecond: 100 });
|
|
60
|
-
let attempts = 0;
|
|
61
|
-
await assert.rejects(async () => {
|
|
62
|
-
await limiter.execute(async () => {
|
|
63
|
-
attempts++;
|
|
64
|
-
throw new Error("AUTH_KEY_UNREGISTERED");
|
|
65
|
-
});
|
|
66
|
-
}, {
|
|
67
|
-
message: "AUTH_KEY_UNREGISTERED",
|
|
68
|
-
});
|
|
69
|
-
assert.strictEqual(attempts, 1, "Should not retry non-retryable errors");
|
|
70
|
-
});
|
|
71
|
-
it("should handle FLOOD_WAIT with seconds parsing", async () => {
|
|
72
|
-
const limiter = new RateLimiter({ maxRetries: 1, maxRequestsPerSecond: 100 });
|
|
73
|
-
await assert.rejects(async () => {
|
|
74
|
-
await limiter.execute(async () => {
|
|
75
|
-
throw new Error("FLOOD_WAIT_1");
|
|
76
|
-
});
|
|
77
|
-
}, {
|
|
78
|
-
message: /Telegram requires 1s wait/,
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import bigInt from "big-integer";
|
|
4
|
-
import { Api } from "telegram/tl/index.js";
|
|
5
|
-
import { reactionToEmoji } from "../telegram-client.js";
|
|
6
|
-
describe("reactionToEmoji", () => {
|
|
7
|
-
it("returns emoticon for ReactionEmoji", () => {
|
|
8
|
-
const r = new Api.ReactionEmoji({ emoticon: "👍" });
|
|
9
|
-
assert.strictEqual(reactionToEmoji(r), "👍");
|
|
10
|
-
});
|
|
11
|
-
it("returns custom:<id> for ReactionCustomEmoji", () => {
|
|
12
|
-
const r = new Api.ReactionCustomEmoji({ documentId: bigInt(12345) });
|
|
13
|
-
assert.strictEqual(reactionToEmoji(r), "custom:12345");
|
|
14
|
-
});
|
|
15
|
-
it("returns star for ReactionPaid", () => {
|
|
16
|
-
const r = new Api.ReactionPaid();
|
|
17
|
-
assert.strictEqual(reactionToEmoji(r), "⭐");
|
|
18
|
-
});
|
|
19
|
-
it("returns null for ReactionEmpty", () => {
|
|
20
|
-
const r = new Api.ReactionEmpty();
|
|
21
|
-
assert.strictEqual(reactionToEmoji(r), null);
|
|
22
|
-
});
|
|
23
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import bigInt from "big-integer";
|
|
4
|
-
import { Api } from "telegram/tl/index.js";
|
|
5
|
-
import { mergeBannedRights, TelegramService } from "../telegram-client.js";
|
|
6
|
-
describe("mergeBannedRights", () => {
|
|
7
|
-
it("preserves omitted flags from current rights", () => {
|
|
8
|
-
const current = { pinMessages: true, inviteUsers: true, sendMessages: false };
|
|
9
|
-
const merged = mergeBannedRights(current, { sendMessages: false });
|
|
10
|
-
assert.strictEqual(merged.sendMessages, true, "user-specified denial applies");
|
|
11
|
-
assert.strictEqual(merged.pinMessages, true, "omitted flag stays banned");
|
|
12
|
-
assert.strictEqual(merged.inviteUsers, true, "omitted flag stays banned");
|
|
13
|
-
assert.strictEqual(merged.sendMedia, false, "omitted flag with no current stays unbanned");
|
|
14
|
-
});
|
|
15
|
-
it("user-provided value overrides current", () => {
|
|
16
|
-
const current = { pinMessages: true };
|
|
17
|
-
const merged = mergeBannedRights(current, { pinMessages: true });
|
|
18
|
-
assert.strictEqual(merged.pinMessages, false, "pinMessages:true allowed -> not banned");
|
|
19
|
-
});
|
|
20
|
-
it("fills missing flags with false when current is undefined", () => {
|
|
21
|
-
const merged = mergeBannedRights(undefined, { sendMessages: true });
|
|
22
|
-
assert.strictEqual(merged.sendMessages, false);
|
|
23
|
-
assert.strictEqual(merged.sendMedia, false);
|
|
24
|
-
assert.strictEqual(merged.pinMessages, false);
|
|
25
|
-
});
|
|
26
|
-
it("covers all nineteen flags (10 exposed + 9 extra preserved)", () => {
|
|
27
|
-
const merged = mergeBannedRights(undefined, {});
|
|
28
|
-
const keys = Object.keys(merged).sort();
|
|
29
|
-
assert.deepStrictEqual(keys, [
|
|
30
|
-
"changeInfo",
|
|
31
|
-
"embedLinks",
|
|
32
|
-
"inviteUsers",
|
|
33
|
-
"manageTopics",
|
|
34
|
-
"pinMessages",
|
|
35
|
-
"sendAudios",
|
|
36
|
-
"sendDocs",
|
|
37
|
-
"sendGames",
|
|
38
|
-
"sendGifs",
|
|
39
|
-
"sendInline",
|
|
40
|
-
"sendMedia",
|
|
41
|
-
"sendMessages",
|
|
42
|
-
"sendPhotos",
|
|
43
|
-
"sendPlain",
|
|
44
|
-
"sendPolls",
|
|
45
|
-
"sendRoundvideos",
|
|
46
|
-
"sendStickers",
|
|
47
|
-
"sendVideos",
|
|
48
|
-
"sendVoices",
|
|
49
|
-
]);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
describe("TelegramService.setChatPermissions", () => {
|
|
53
|
-
it("merges new permissions with existing defaultBannedRights", async () => {
|
|
54
|
-
const existingRights = new Api.ChatBannedRights({
|
|
55
|
-
untilDate: 0,
|
|
56
|
-
pinMessages: true,
|
|
57
|
-
inviteUsers: true,
|
|
58
|
-
});
|
|
59
|
-
const channel = new Api.Channel({
|
|
60
|
-
id: bigInt(12345),
|
|
61
|
-
title: "test",
|
|
62
|
-
photo: new Api.ChatPhotoEmpty(),
|
|
63
|
-
date: 0,
|
|
64
|
-
accessHash: bigInt(1),
|
|
65
|
-
defaultBannedRights: existingRights,
|
|
66
|
-
});
|
|
67
|
-
const inputChannel = new Api.InputPeerChannel({ channelId: bigInt(12345), accessHash: bigInt(1) });
|
|
68
|
-
const invocations = [];
|
|
69
|
-
const fakeClient = {
|
|
70
|
-
invoke: async (req) => {
|
|
71
|
-
invocations.push(req);
|
|
72
|
-
if (req instanceof Api.channels.GetFullChannel) {
|
|
73
|
-
return new Api.messages.ChatFull({
|
|
74
|
-
fullChat: new Api.ChannelFull({
|
|
75
|
-
id: bigInt(12345),
|
|
76
|
-
about: "",
|
|
77
|
-
readInboxMaxId: 0,
|
|
78
|
-
readOutboxMaxId: 0,
|
|
79
|
-
unreadCount: 0,
|
|
80
|
-
chatPhoto: new Api.PhotoEmpty({ id: bigInt(0) }),
|
|
81
|
-
notifySettings: new Api.PeerNotifySettings({}),
|
|
82
|
-
pts: 0,
|
|
83
|
-
botInfo: [],
|
|
84
|
-
}),
|
|
85
|
-
chats: [channel],
|
|
86
|
-
users: [],
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
return undefined;
|
|
90
|
-
},
|
|
91
|
-
getInputEntity: async () => inputChannel,
|
|
92
|
-
};
|
|
93
|
-
const service = new TelegramService(1, "hash");
|
|
94
|
-
const internals = service;
|
|
95
|
-
internals.client = fakeClient;
|
|
96
|
-
internals.connected = true;
|
|
97
|
-
internals.resolveChat = async () => channel;
|
|
98
|
-
await service.setChatPermissions("12345", { sendMessages: false });
|
|
99
|
-
const editCall = invocations.find((r) => r instanceof Api.messages.EditChatDefaultBannedRights);
|
|
100
|
-
assert.ok(editCall, "EditChatDefaultBannedRights was invoked");
|
|
101
|
-
const rights = editCall.bannedRights;
|
|
102
|
-
assert.strictEqual(rights.sendMessages, true, "sendMessages becomes banned");
|
|
103
|
-
assert.strictEqual(rights.pinMessages, true, "pinMessages stays banned (preserved)");
|
|
104
|
-
assert.strictEqual(rights.inviteUsers, true, "inviteUsers stays banned (preserved)");
|
|
105
|
-
assert.strictEqual(rights.sendMedia, false, "omitted flag with no prior value stays unbanned");
|
|
106
|
-
});
|
|
107
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import { DESTRUCTIVE, fail, formatReactions, ok, READ_ONLY, sanitize, WRITE } from "../../tools/shared.js";
|
|
4
|
-
describe("shared utilities", () => {
|
|
5
|
-
describe("ok()", () => {
|
|
6
|
-
it("should return success response with text content", () => {
|
|
7
|
-
const result = ok("Operation successful");
|
|
8
|
-
assert.deepStrictEqual(result, {
|
|
9
|
-
content: [{ type: "text", text: "Operation successful" }],
|
|
10
|
-
});
|
|
11
|
-
});
|
|
12
|
-
it("should handle empty string", () => {
|
|
13
|
-
const result = ok("");
|
|
14
|
-
assert.deepStrictEqual(result, {
|
|
15
|
-
content: [{ type: "text", text: "" }],
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
});
|
|
19
|
-
describe("fail()", () => {
|
|
20
|
-
it("should return error response with isError flag", () => {
|
|
21
|
-
const error = new Error("Something went wrong");
|
|
22
|
-
const result = fail(error);
|
|
23
|
-
assert.deepStrictEqual(result, {
|
|
24
|
-
content: [{ type: "text", text: "Error: Something went wrong" }],
|
|
25
|
-
isError: true,
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
it("should handle non-Error objects", () => {
|
|
29
|
-
const result = fail({ message: "Custom error" });
|
|
30
|
-
assert.ok(result.content[0].text.includes("Error:"));
|
|
31
|
-
assert.strictEqual(result.isError, true);
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
describe("sanitize()", () => {
|
|
35
|
-
it("should remove unpaired high surrogates", () => {
|
|
36
|
-
const input = "Hello\uD800World";
|
|
37
|
-
const result = sanitize(input);
|
|
38
|
-
assert.strictEqual(result, "Hello\uFFFDWorld");
|
|
39
|
-
});
|
|
40
|
-
it("should remove unpaired low surrogates", () => {
|
|
41
|
-
const input = "Hello\uDC00World";
|
|
42
|
-
const result = sanitize(input);
|
|
43
|
-
assert.strictEqual(result, "Hello\uFFFDWorld");
|
|
44
|
-
});
|
|
45
|
-
it("should preserve valid surrogate pairs", () => {
|
|
46
|
-
const input = "Hello\uD83D\uDE00World"; // 😀 emoji
|
|
47
|
-
const result = sanitize(input);
|
|
48
|
-
assert.strictEqual(result, "Hello\uD83D\uDE00World");
|
|
49
|
-
});
|
|
50
|
-
it("should handle normal text without surrogates", () => {
|
|
51
|
-
const input = "Hello World";
|
|
52
|
-
const result = sanitize(input);
|
|
53
|
-
assert.strictEqual(result, "Hello World");
|
|
54
|
-
});
|
|
55
|
-
it("should handle empty string", () => {
|
|
56
|
-
const result = sanitize("");
|
|
57
|
-
assert.strictEqual(result, "");
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
describe("formatReactions()", () => {
|
|
61
|
-
it("should format reactions with counts", () => {
|
|
62
|
-
const reactions = [
|
|
63
|
-
{ emoji: "👍", count: 5, me: false },
|
|
64
|
-
{ emoji: "❤️", count: 3, me: true },
|
|
65
|
-
{ emoji: "🔥", count: 1, me: false },
|
|
66
|
-
];
|
|
67
|
-
const result = formatReactions(reactions);
|
|
68
|
-
assert.strictEqual(result, " [👍×5 ❤️×3(me) 🔥×1]");
|
|
69
|
-
});
|
|
70
|
-
it("should mark reactions from current user", () => {
|
|
71
|
-
const reactions = [{ emoji: "👍", count: 2, me: true }];
|
|
72
|
-
const result = formatReactions(reactions);
|
|
73
|
-
assert.strictEqual(result, " [👍×2(me)]");
|
|
74
|
-
});
|
|
75
|
-
it("should return empty string for undefined reactions", () => {
|
|
76
|
-
const result = formatReactions(undefined);
|
|
77
|
-
assert.strictEqual(result, "");
|
|
78
|
-
});
|
|
79
|
-
it("should return empty string for empty reactions array", () => {
|
|
80
|
-
const result = formatReactions([]);
|
|
81
|
-
assert.strictEqual(result, "");
|
|
82
|
-
});
|
|
83
|
-
it("should handle single reaction", () => {
|
|
84
|
-
const reactions = [{ emoji: "🎉", count: 1, me: false }];
|
|
85
|
-
const result = formatReactions(reactions);
|
|
86
|
-
assert.strictEqual(result, " [🎉×1]");
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
describe("MCP tool annotations", () => {
|
|
90
|
-
it("should define READ_ONLY preset", () => {
|
|
91
|
-
assert.deepStrictEqual(READ_ONLY, {
|
|
92
|
-
readOnlyHint: true,
|
|
93
|
-
openWorldHint: true,
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
it("should define WRITE preset", () => {
|
|
97
|
-
assert.deepStrictEqual(WRITE, {
|
|
98
|
-
readOnlyHint: false,
|
|
99
|
-
openWorldHint: true,
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
it("should define DESTRUCTIVE preset", () => {
|
|
103
|
-
assert.deepStrictEqual(DESTRUCTIVE, {
|
|
104
|
-
readOnlyHint: false,
|
|
105
|
-
destructiveHint: true,
|
|
106
|
-
openWorldHint: true,
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
});
|