Refactor Telegram MCP integration and add new API functionalities

- Updated import paths for Telegram types and API functions to improve module organization.
- Introduced new helper functions for reading typed values from MCP tool arguments.
- Added comprehensive API functions for managing Telegram contacts, chats, and messages, including creating groups, channels, and polls.
- Implemented a tool action parser to convert tool inputs into human-readable descriptions.
- Added tests for new functionalities to ensure reliability and correctness.

This update enhances the Telegram MCP server's capabilities and improves code maintainability.
This commit is contained in:
Steven Enamakel
2026-01-31 00:51:52 +05:30
parent c270f44c08
commit 95e06bc54a
195 changed files with 1180 additions and 1012 deletions
+19
View File
@@ -14,6 +14,7 @@
"@tauri-apps/plugin-opener": "^2",
"@types/react-router-dom": "^5.3.3",
"buffer": "^6.0.3",
"debug": "^4.4.3",
"lottie-react": "^2.4.1",
"os-browserify": "^0.3.0",
"process": "^0.11.10",
@@ -33,6 +34,7 @@
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/typography": "^0.5.19",
"@tauri-apps/cli": "^2",
"@types/debug": "^4.1.12",
"@types/node": "^25.0.10",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
@@ -1663,6 +1665,16 @@
"assertion-error": "^2.0.1"
}
},
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/ms": "*"
}
},
"node_modules/@types/deep-eql": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
@@ -1683,6 +1695,13 @@
"integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==",
"license": "MIT"
},
"node_modules/@types/ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.0.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz",
+2
View File
@@ -22,6 +22,7 @@
"@tauri-apps/plugin-opener": "^2",
"@types/react-router-dom": "^5.3.3",
"buffer": "^6.0.3",
"debug": "^4.4.3",
"lottie-react": "^2.4.1",
"os-browserify": "^0.3.0",
"process": "^0.11.10",
@@ -41,6 +42,7 @@
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/typography": "^0.5.19",
"@tauri-apps/cli": "^2",
"@types/debug": "^4.1.12",
"@types/node": "^25.0.10",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
+3 -3
View File
@@ -2,8 +2,8 @@
* Factory builder for TelegramMCPContext (used by tool handlers).
*/
import { vi } from 'vitest';
import type { TelegramMCPContext } from "../../lib/mcp/telegram/types";
import { vi } from "vitest";
import type { TelegramMCPContext } from "../../lib/telegram/types";
import type { TelegramState } from "../../store/telegram/types";
import { initialState } from "../../store/telegram/types";
@@ -18,7 +18,7 @@ function createMockTransport() {
}
export function createMockContext(
telegramOverrides: Partial<TelegramState> = {},
telegramOverrides: Partial<TelegramState> = {}
): TelegramMCPContext {
return {
telegramState: { ...initialState, ...telegramOverrides },
+11 -8
View File
@@ -7,7 +7,7 @@
import type { MCPTool, MCPToolResult } from "../types";
import type { ExtraTool } from "./types";
import { sendMessage } from "../telegram/api/sendMessage";
import { sendMessage } from "../../telegram/api/sendMessage";
// ---------------------------------------------------------------------------
// Constants
@@ -148,7 +148,7 @@ IMPORTANT: Bulk operations execute sequentially with delays between items to avo
export async function executeBulkTool(
toolName: string,
args: Record<string, unknown>,
args: Record<string, unknown>
): Promise<MCPToolResult> {
switch (toolName) {
case "bulk_send_message":
@@ -174,7 +174,7 @@ export async function executeBulkTool(
// ---------------------------------------------------------------------------
async function executeBulkSendMessage(
args: Record<string, unknown>,
args: Record<string, unknown>
): Promise<MCPToolResult> {
const chatIds = args.chat_ids as string[];
const message = args.message as string;
@@ -182,7 +182,10 @@ async function executeBulkSendMessage(
if (!chatIds?.length || !message) {
return {
content: [
{ type: "text", text: "chat_ids (array) and message (string) are required" },
{
type: "text",
text: "chat_ids (array) and message (string) are required",
},
],
isError: true,
};
@@ -217,7 +220,7 @@ async function executeBulkSendMessage(
}
async function executeBulkArchiveChats(
args: Record<string, unknown>,
args: Record<string, unknown>
): Promise<MCPToolResult> {
const chatIds = args.chat_ids as string[];
if (!chatIds?.length) {
@@ -240,7 +243,7 @@ async function executeBulkArchiveChats(
}
async function executeBulkMarkAsRead(
args: Record<string, unknown>,
args: Record<string, unknown>
): Promise<MCPToolResult> {
const chatIds = args.chat_ids as string[];
if (!chatIds?.length) {
@@ -262,7 +265,7 @@ async function executeBulkMarkAsRead(
}
async function executeBulkDeleteMessages(
args: Record<string, unknown>,
args: Record<string, unknown>
): Promise<MCPToolResult> {
const chatId = args.chat_id as string;
const messageIds = args.message_ids as string[];
@@ -291,7 +294,7 @@ async function executeBulkDeleteMessages(
}
async function executeBulkForwardMessages(
args: Record<string, unknown>,
args: Record<string, unknown>
): Promise<MCPToolResult> {
const fromChatId = args.from_chat_id as string;
const messageIds = args.message_ids as string[];
@@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { getChats } from "../getChats";
import type { TelegramChat } from "../../../../../store/telegram/types";
import type { TelegramChat } from "../../../../store/telegram/types";
// Mock dependencies
const mockClient = {
@@ -10,7 +10,7 @@ const mockClient = {
sendMessage: vi.fn(),
};
vi.mock("../../../../../services/mtprotoService", () => ({
vi.mock("../../../../services/mtprotoService", () => ({
mtprotoService: {
getClient: vi.fn(() => mockClient),
withFloodWaitHandling: vi.fn(async (fn: any) => await fn()),
@@ -20,7 +20,7 @@ vi.mock("../../../../../services/mtprotoService", () => ({
},
}));
vi.mock("../../../../../store", () => ({
vi.mock("../../../../store", () => ({
store: {
getState: vi.fn(() => ({
user: { user: { _id: "u1" } },
@@ -39,7 +39,7 @@ vi.mock("../../../../../store", () => ({
},
}));
vi.mock("../../../../../store/telegramSelectors", () => ({
vi.mock("../../../../store/telegramSelectors", () => ({
selectTelegramUserState: vi.fn(() => ({
chats: {},
chatsOrder: [],
@@ -51,7 +51,7 @@ vi.mock("../../../../../store/telegramSelectors", () => ({
selectOrderedChats: vi.fn(() => []),
}));
vi.mock("../../../rateLimiter", () => ({
vi.mock("../../../mcp/rateLimiter", () => ({
enforceRateLimit: vi.fn(),
resetRequestCallCount: vi.fn(),
}));
@@ -97,7 +97,7 @@ describe("getChats", () => {
expect(result.fromCache).toBe(true);
const { mtprotoService } = await import(
"../../../../../services/mtprotoService"
"../../../../services/mtprotoService"
);
expect(mtprotoService.getClient).not.toHaveBeenCalled();
});
@@ -106,7 +106,7 @@ describe("getChats", () => {
const { getOrderedChats } = await import("../helpers");
vi.mocked(getOrderedChats).mockReturnValueOnce([]);
const { enforceRateLimit } = await import("../../../rateLimiter");
const { enforceRateLimit } = await import("../../../mcp/rateLimiter");
vi.mocked(enforceRateLimit).mockResolvedValueOnce(undefined);
const mockApiDialogs = {
@@ -145,7 +145,7 @@ describe("getChats", () => {
const { getOrderedChats } = await import("../helpers");
vi.mocked(getOrderedChats).mockReturnValueOnce([]);
const { enforceRateLimit } = await import("../../../rateLimiter");
const { enforceRateLimit } = await import("../../../mcp/rateLimiter");
vi.mocked(enforceRateLimit).mockResolvedValueOnce(undefined);
mockClient.invoke.mockRejectedValueOnce(new Error("API Error"));
@@ -160,7 +160,7 @@ describe("getChats", () => {
const { getOrderedChats } = await import("../helpers");
vi.mocked(getOrderedChats).mockReturnValueOnce([]);
const { enforceRateLimit } = await import("../../../rateLimiter");
const { enforceRateLimit } = await import("../../../mcp/rateLimiter");
vi.mocked(enforceRateLimit).mockRejectedValueOnce(
new Error("Rate limit exceeded")
);
@@ -171,22 +171,27 @@ describe("getChats", () => {
expect(result.fromCache).toBe(false);
const { mtprotoService } = await import(
"../../../../../services/mtprotoService"
"../../../../services/mtprotoService"
);
expect(mtprotoService.getClient).not.toHaveBeenCalled();
});
it("should respect custom limit parameter", async () => {
const mockCachedChats: TelegramChat[] = Array.from({ length: 50 }, (_, i) => ({
id: String(i),
title: `Chat ${i}`,
type: "private",
unreadCount: 0,
isPinned: false,
}));
const mockCachedChats: TelegramChat[] = Array.from(
{ length: 50 },
(_, i) => ({
id: String(i),
title: `Chat ${i}`,
type: "private",
unreadCount: 0,
isPinned: false,
})
);
const { getOrderedChats } = await import("../helpers");
vi.mocked(getOrderedChats).mockReturnValueOnce(mockCachedChats.slice(0, 50));
vi.mocked(getOrderedChats).mockReturnValueOnce(
mockCachedChats.slice(0, 50)
);
const result = await getChats(50);
@@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { getCurrentUser } from "../getCurrentUser";
import type { TelegramUser } from "../../../../../store/telegram/types";
import type { TelegramUser } from "../../../../store/telegram/types";
// Mock dependencies
const mockClient = {
@@ -10,7 +10,7 @@ const mockClient = {
sendMessage: vi.fn(),
};
vi.mock("../../../../../services/mtprotoService", () => ({
vi.mock("../../../../services/mtprotoService", () => ({
mtprotoService: {
getClient: vi.fn(() => mockClient),
withFloodWaitHandling: vi.fn(async (fn: any) => await fn()),
@@ -20,7 +20,7 @@ vi.mock("../../../../../services/mtprotoService", () => ({
},
}));
vi.mock("../../../../../store", () => ({
vi.mock("../../../../store", () => ({
store: {
getState: vi.fn(() => ({
user: { user: { _id: "u1" } },
@@ -40,7 +40,7 @@ vi.mock("../../../../../store", () => ({
},
}));
vi.mock("../../../../../store/telegramSelectors", () => ({
vi.mock("../../../../store/telegramSelectors", () => ({
selectTelegramUserState: vi.fn(() => ({
chats: {},
chatsOrder: [],
@@ -52,7 +52,7 @@ vi.mock("../../../../../store/telegramSelectors", () => ({
selectOrderedChats: vi.fn(() => []),
}));
vi.mock("../../../rateLimiter", () => ({
vi.mock("../../../mcp/rateLimiter", () => ({
enforceRateLimit: vi.fn(),
resetRequestCallCount: vi.fn(),
}));
@@ -89,7 +89,7 @@ describe("getCurrentUser", () => {
expect(result.fromCache).toBe(true);
const { mtprotoService } = await import(
"../../../../../services/mtprotoService"
"../../../../services/mtprotoService"
);
expect(mtprotoService.getClient).not.toHaveBeenCalled();
});
@@ -98,7 +98,7 @@ describe("getCurrentUser", () => {
const { getCurrentUser: getCurrentUserHelper } = await import("../helpers");
vi.mocked(getCurrentUserHelper).mockReturnValueOnce(undefined);
const { enforceRateLimit } = await import("../../../rateLimiter");
const { enforceRateLimit } = await import("../../../mcp/rateLimiter");
vi.mocked(enforceRateLimit).mockResolvedValueOnce(undefined);
const mockApiUser = {
@@ -124,7 +124,7 @@ describe("getCurrentUser", () => {
const { getCurrentUser: getCurrentUserHelper } = await import("../helpers");
vi.mocked(getCurrentUserHelper).mockReturnValueOnce(undefined);
const { enforceRateLimit } = await import("../../../rateLimiter");
const { enforceRateLimit } = await import("../../../mcp/rateLimiter");
vi.mocked(enforceRateLimit).mockResolvedValueOnce(undefined);
mockClient.getMe.mockResolvedValueOnce(null);
@@ -139,7 +139,7 @@ describe("getCurrentUser", () => {
const { getCurrentUser: getCurrentUserHelper } = await import("../helpers");
vi.mocked(getCurrentUserHelper).mockReturnValueOnce(undefined);
const { enforceRateLimit } = await import("../../../rateLimiter");
const { enforceRateLimit } = await import("../../../mcp/rateLimiter");
vi.mocked(enforceRateLimit).mockResolvedValueOnce(undefined);
mockClient.getMe.mockRejectedValueOnce(new Error("API Error"));
@@ -154,7 +154,7 @@ describe("getCurrentUser", () => {
const { getCurrentUser: getCurrentUserHelper } = await import("../helpers");
vi.mocked(getCurrentUserHelper).mockReturnValueOnce(undefined);
const { enforceRateLimit } = await import("../../../rateLimiter");
const { enforceRateLimit } = await import("../../../mcp/rateLimiter");
vi.mocked(enforceRateLimit).mockRejectedValueOnce(
new Error("Rate limit exceeded")
);
@@ -165,7 +165,7 @@ describe("getCurrentUser", () => {
expect(result.fromCache).toBe(false);
const { mtprotoService } = await import(
"../../../../../services/mtprotoService"
"../../../../services/mtprotoService"
);
expect(mtprotoService.getClient).not.toHaveBeenCalled();
});
@@ -174,7 +174,7 @@ describe("getCurrentUser", () => {
const { getCurrentUser: getCurrentUserHelper } = await import("../helpers");
vi.mocked(getCurrentUserHelper).mockReturnValueOnce(undefined);
const { enforceRateLimit } = await import("../../../rateLimiter");
const { enforceRateLimit } = await import("../../../mcp/rateLimiter");
vi.mocked(enforceRateLimit).mockResolvedValueOnce(undefined);
const mockApiUser = {
@@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { getMessages } from "../getMessages";
import type { TelegramMessage } from "../../../../../store/telegram/types";
import type { TelegramMessage } from "../../../../store/telegram/types";
// Mock dependencies
const mockClient = {
@@ -10,7 +10,7 @@ const mockClient = {
sendMessage: vi.fn(),
};
vi.mock("../../../../../services/mtprotoService", () => ({
vi.mock("../../../../services/mtprotoService", () => ({
mtprotoService: {
getClient: vi.fn(() => mockClient),
withFloodWaitHandling: vi.fn(async (fn: any) => await fn()),
@@ -20,7 +20,7 @@ vi.mock("../../../../../services/mtprotoService", () => ({
},
}));
vi.mock("../../../../../store", () => ({
vi.mock("../../../../store", () => ({
store: {
getState: vi.fn(() => ({
user: { user: { _id: "u1" } },
@@ -39,7 +39,7 @@ vi.mock("../../../../../store", () => ({
},
}));
vi.mock("../../../../../store/telegramSelectors", () => ({
vi.mock("../../../../store/telegramSelectors", () => ({
selectTelegramUserState: vi.fn(() => ({
chats: {},
chatsOrder: [],
@@ -51,7 +51,7 @@ vi.mock("../../../../../store/telegramSelectors", () => ({
selectOrderedChats: vi.fn(() => []),
}));
vi.mock("../../../rateLimiter", () => ({
vi.mock("../../../mcp/rateLimiter", () => ({
enforceRateLimit: vi.fn(),
resetRequestCallCount: vi.fn(),
}));
@@ -112,7 +112,7 @@ describe("getMessages", () => {
expect(result.fromCache).toBe(true);
const { mtprotoService } = await import(
"../../../../../services/mtprotoService"
"../../../../services/mtprotoService"
);
expect(mtprotoService.getClient).not.toHaveBeenCalled();
});
@@ -128,7 +128,7 @@ describe("getMessages", () => {
isPinned: false,
});
const { enforceRateLimit } = await import("../../../rateLimiter");
const { enforceRateLimit } = await import("../../../mcp/rateLimiter");
vi.mocked(enforceRateLimit).mockResolvedValueOnce(undefined);
const mockApiMessages = {
@@ -151,7 +151,10 @@ describe("getMessages", () => {
],
};
mockClient.getInputEntity.mockResolvedValueOnce({ className: "InputPeerUser", userId: 1n });
mockClient.getInputEntity.mockResolvedValueOnce({
className: "InputPeerUser",
userId: 1n,
});
mockClient.invoke.mockResolvedValueOnce(mockApiMessages);
const result = await getMessages("1", 20);
@@ -176,7 +179,7 @@ describe("getMessages", () => {
expect(result.fromCache).toBe(false);
const { mtprotoService } = await import(
"../../../../../services/mtprotoService"
"../../../../services/mtprotoService"
);
expect(mtprotoService.getClient).not.toHaveBeenCalled();
});
@@ -192,10 +195,13 @@ describe("getMessages", () => {
isPinned: false,
});
const { enforceRateLimit } = await import("../../../rateLimiter");
const { enforceRateLimit } = await import("../../../mcp/rateLimiter");
vi.mocked(enforceRateLimit).mockResolvedValueOnce(undefined);
mockClient.getInputEntity.mockResolvedValueOnce({ className: "InputPeerUser", userId: 1n });
mockClient.getInputEntity.mockResolvedValueOnce({
className: "InputPeerUser",
userId: 1n,
});
mockClient.invoke.mockRejectedValueOnce(new Error("API Error"));
const result = await getMessages("1", 20);
@@ -215,7 +221,7 @@ describe("getMessages", () => {
isPinned: false,
});
const { enforceRateLimit } = await import("../../../rateLimiter");
const { enforceRateLimit } = await import("../../../mcp/rateLimiter");
vi.mocked(enforceRateLimit).mockRejectedValueOnce(
new Error("Rate limit exceeded")
);
@@ -226,7 +232,7 @@ describe("getMessages", () => {
expect(result.fromCache).toBe(false);
const { mtprotoService } = await import(
"../../../../../services/mtprotoService"
"../../../../services/mtprotoService"
);
expect(mtprotoService.getClient).not.toHaveBeenCalled();
});
@@ -10,7 +10,7 @@ import type {
TelegramChat,
TelegramUser,
TelegramMessage,
} from "../../../../../store/telegram/types";
} from "../../../../store/telegram/types";
// Mock the store module
vi.mock("../../../../../store", () => ({
@@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
import { sendMessage } from "../sendMessage";
// Mock dependencies
vi.mock("../../../../../services/mtprotoService", () => {
vi.mock("../../../../services/mtprotoService", () => {
const mockClient = {
invoke: vi.fn(),
getInputEntity: vi.fn(),
@@ -21,7 +21,7 @@ vi.mock("../../../../../services/mtprotoService", () => {
};
});
vi.mock("../../../../../store", () => ({
vi.mock("../../../../store", () => ({
store: {
getState: vi.fn(() => ({
user: { user: { _id: "u1" } },
@@ -40,7 +40,7 @@ vi.mock("../../../../../store", () => ({
},
}));
vi.mock("../../../../../store/telegramSelectors", () => ({
vi.mock("../../../../store/telegramSelectors", () => ({
selectTelegramUserState: vi.fn(() => ({
chats: {},
chatsOrder: [],
@@ -52,7 +52,7 @@ vi.mock("../../../../../store/telegramSelectors", () => ({
selectOrderedChats: vi.fn(() => []),
}));
vi.mock("../../../rateLimiter", () => ({
vi.mock("../../../mcp/rateLimiter", () => ({
enforceRateLimit: vi.fn(),
resetRequestCallCount: vi.fn(),
}));
@@ -71,7 +71,7 @@ describe("sendMessage", () => {
beforeEach(async () => {
vi.clearAllMocks();
const mtproto = await import("../../../../../services/mtprotoService");
const mtproto = await import("../../../../services/mtprotoService");
mockMtprotoService = mtproto.mtprotoService;
mockClient = mockMtprotoService.getClient();
});
@@ -203,7 +203,10 @@ describe("sendMessage", () => {
const result = await sendMessage("1", "");
expect(mockMtprotoService.sendMessage).toHaveBeenCalledWith("@testuser", "");
expect(mockMtprotoService.sendMessage).toHaveBeenCalledWith(
"@testuser",
""
);
expect(result.data).toBeDefined();
});
@@ -225,7 +228,10 @@ describe("sendMessage", () => {
const result = await sendMessage("1", longMessage);
expect(mockMtprotoService.sendMessage).toHaveBeenCalledWith("1", longMessage);
expect(mockMtprotoService.sendMessage).toHaveBeenCalledWith(
"1",
longMessage
);
expect(result.data).toBeDefined();
});
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import bigInt from "big-integer";
@@ -6,7 +6,7 @@ import bigInt from "big-integer";
export async function addContact(
phone: string,
firstName: string,
lastName?: string,
lastName?: string
): Promise<ApiResult<void>> {
if (!phone) {
throw new Error("phone is required");
@@ -28,7 +28,7 @@ export async function addContact(
lastName: lastName ?? "",
}),
],
}),
})
);
});
@@ -1,10 +1,10 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { getChatById } from "./helpers";
import type { ApiResult } from "./types";
import { Api } from "telegram";
export async function archiveChat(
chatId: string | number,
chatId: string | number
): Promise<ApiResult<void>> {
const chat = getChatById(chatId);
if (!chat) throw new Error(`Chat not found: ${chatId}`);
@@ -22,7 +22,7 @@ export async function archiveChat(
folderId: 1, // 1 = Archive folder
}),
],
}),
})
);
});
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import { toInputChannel, toInputPeer } from "./apiCastHelpers";
@@ -6,7 +6,7 @@ import { getChatById } from "./helpers";
export async function banUser(
chatId: string | number,
userId: string | number,
userId: string | number
): Promise<ApiResult<void>> {
const chat = getChatById(chatId);
if (!chat) {
@@ -38,7 +38,7 @@ export async function banUser(
embedLinks: true,
untilDate: 0,
}),
}),
})
);
});
@@ -1,9 +1,11 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import { toInputPeer } from "./apiCastHelpers";
export async function blockUser(userId: string | number): Promise<ApiResult<void>> {
export async function blockUser(
userId: string | number
): Promise<ApiResult<void>> {
const client = mtprotoService.getClient();
await mtprotoService.withFloodWaitHandling(async () => {
@@ -2,13 +2,13 @@
* API: Clear draft
*/
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { getChatById } from "./helpers";
import type { ApiResult } from "./types";
import { Api } from "telegram";
export async function clearDraft(
chatId: string | number,
chatId: string | number
): Promise<ApiResult<void>> {
const chat = getChatById(chatId);
if (!chat) {
@@ -16,9 +16,7 @@ export async function clearDraft(
}
const client = mtprotoService.getClient();
const entity = chat.username
? `@${chat.username.replace("@", "")}`
: chat.id;
const entity = chat.username ? `@${chat.username.replace("@", "")}` : chat.id;
await mtprotoService.withFloodWaitHandling(async () => {
const inputPeer = await client.getInputEntity(entity);
@@ -26,7 +24,7 @@ export async function clearDraft(
new Api.messages.SaveDraft({
peer: inputPeer,
message: "",
}),
})
);
});
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import type { ResultWithChats } from "./apiResultTypes";
@@ -7,7 +7,7 @@ import { narrow } from "./apiCastHelpers";
export async function createChannel(
title: string,
about?: string,
megagroup?: boolean,
megagroup?: boolean
): Promise<ApiResult<{ id: string; type: string }>> {
const client = mtprotoService.getClient();
@@ -18,7 +18,7 @@ export async function createChannel(
about: about ?? "",
megagroup: megagroup ?? false,
broadcast: !megagroup,
}),
})
);
});
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import type { ResultWithChats } from "./apiResultTypes";
@@ -6,7 +6,7 @@ import { toInputUser, narrow } from "./apiCastHelpers";
export async function createGroup(
title: string,
userIds: string[],
userIds: string[]
): Promise<ApiResult<{ id: string }>> {
if (userIds.length === 0) {
throw new Error("user_ids must not be empty");
@@ -2,7 +2,7 @@
* API: Create poll
*/
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { getChatById } from "./helpers";
import type { ApiResult } from "./types";
import { Api } from "telegram";
@@ -11,7 +11,7 @@ import bigInt from "big-integer";
export async function createPoll(
chatId: string | number,
question: string,
options: string[],
options: string[]
): Promise<ApiResult<void>> {
const chat = getChatById(chatId);
if (!chat) {
@@ -19,9 +19,7 @@ export async function createPoll(
}
const client = mtprotoService.getClient();
const entity = chat.username
? `@${chat.username.replace("@", "")}`
: chat.id;
const entity = chat.username ? `@${chat.username.replace("@", "")}` : chat.id;
await mtprotoService.withFloodWaitHandling(async () => {
const inputPeer = await client.getInputEntity(entity);
@@ -40,13 +38,13 @@ export async function createPoll(
new Api.PollAnswer({
text: new Api.TextWithEntities({ text: opt, entities: [] }),
option: Buffer.from([i]),
}),
})
),
}),
}),
message: "",
randomId: bigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)),
}),
})
);
});
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { getChatById } from "./helpers";
import type { ApiResult } from "./types";
import { Api } from "telegram";
@@ -6,7 +6,7 @@ import bigInt from "big-integer";
import { toInputChannel } from "./apiCastHelpers";
export async function deleteChatPhoto(
chatId: string | number,
chatId: string | number
): Promise<ApiResult<void>> {
const chat = getChatById(chatId);
if (!chat) throw new Error(`Chat not found: ${chatId}`);
@@ -21,7 +21,7 @@ export async function deleteChatPhoto(
new Api.channels.EditPhoto({
channel: toInputChannel(inputChannel),
photo: new Api.InputChatPhotoEmpty(),
}),
})
);
});
} else {
@@ -30,7 +30,7 @@ export async function deleteChatPhoto(
new Api.messages.EditChatPhoto({
chatId: bigInt(chat.id),
photo: new Api.InputChatPhotoEmpty(),
}),
})
);
});
}
@@ -1,9 +1,11 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import { toInputUser } from "./apiCastHelpers";
export async function deleteContact(userId: string | number): Promise<ApiResult<void>> {
export async function deleteContact(
userId: string | number
): Promise<ApiResult<void>> {
const client = mtprotoService.getClient();
await mtprotoService.withFloodWaitHandling(async () => {
@@ -11,7 +13,7 @@ export async function deleteContact(userId: string | number): Promise<ApiResult<
await client.invoke(
new Api.contacts.DeleteContacts({
id: [toInputUser(inputUser)],
}),
})
);
});
@@ -2,22 +2,20 @@
* API: Delete message
*/
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { getChatById } from "./helpers";
import type { ApiResult } from "./types";
export async function deleteMessage(
chatId: string | number,
messageId: number,
messageId: number
): Promise<ApiResult<void>> {
const chat = getChatById(chatId);
if (!chat) {
throw new Error(`Chat not found: ${chatId}`);
}
const entity = chat.username
? `@${chat.username.replace("@", "")}`
: chat.id;
const entity = chat.username ? `@${chat.username.replace("@", "")}` : chat.id;
const client = mtprotoService.getClient();
await mtprotoService.withFloodWaitHandling(async () => {
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import bigInt from "big-integer";
@@ -15,7 +15,7 @@ export async function deleteProfilePhoto(): Promise<ApiResult<void>> {
offset: 0,
maxId: bigInt(0),
limit: 1,
}),
})
);
});
@@ -40,7 +40,7 @@ export async function deleteProfilePhoto(): Promise<ApiResult<void>> {
fileReference: photo.fileReference,
}),
],
}),
})
);
});
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import { toInputChannel, toInputUser } from "./apiCastHelpers";
@@ -6,7 +6,7 @@ import { getChatById } from "./helpers";
export async function demoteAdmin(
chatId: string | number,
userId: string | number,
userId: string | number
): Promise<ApiResult<void>> {
const chat = getChatById(chatId);
if (!chat) {
@@ -15,7 +15,7 @@ export async function demoteAdmin(
if (chat.type !== "channel" && chat.type !== "supergroup") {
throw new Error(
"Admin demotion is only available for channels/supergroups.",
"Admin demotion is only available for channels/supergroups."
);
}
@@ -31,7 +31,7 @@ export async function demoteAdmin(
userId: toInputUser(inputUser),
adminRights: new Api.ChatAdminRights({}),
rank: "",
}),
})
);
});
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { getChatById } from "./helpers";
import type { ApiResult } from "./types";
import { Api } from "telegram";
@@ -7,7 +7,7 @@ import { toInputChannel } from "./apiCastHelpers";
export async function editChatTitle(
chatId: string | number,
title: string,
title: string
): Promise<ApiResult<void>> {
const chat = getChatById(chatId);
if (!chat) throw new Error(`Chat not found: ${chatId}`);
@@ -22,7 +22,7 @@ export async function editChatTitle(
new Api.channels.EditTitle({
channel: toInputChannel(inputChannel),
title,
}),
})
);
});
} else {
@@ -31,7 +31,7 @@ export async function editChatTitle(
new Api.messages.EditChatTitle({
chatId: bigInt(chat.id),
title,
}),
})
);
});
}
@@ -2,23 +2,21 @@
* API: Edit message
*/
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { getChatById } from "./helpers";
import type { ApiResult } from "./types";
export async function editMessage(
chatId: string | number,
messageId: number,
newText: string,
newText: string
): Promise<ApiResult<void>> {
const chat = getChatById(chatId);
if (!chat) {
throw new Error(`Chat not found: ${chatId}`);
}
const entity = chat.username
? `@${chat.username.replace("@", "")}`
: chat.id;
const entity = chat.username ? `@${chat.username.replace("@", "")}` : chat.id;
const client = mtprotoService.getClient();
await mtprotoService.withFloodWaitHandling(async () => {
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { getChatById } from "./helpers";
import type { ApiResult } from "./types";
import { Api } from "telegram";
@@ -9,7 +9,7 @@ export async function exportChatInvite(
chatId: string | number,
title?: string,
expireDate?: number,
usageLimit?: number,
usageLimit?: number
): Promise<ApiResult<{ link: string }>> {
const chat = getChatById(chatId);
if (!chat) throw new Error(`Chat not found: ${chatId}`);
@@ -25,7 +25,7 @@ export async function exportChatInvite(
title: title ?? undefined,
expireDate: expireDate ?? undefined,
usageLimit: usageLimit ?? undefined,
}),
})
);
});
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import bigInt from "big-integer";
@@ -12,9 +12,7 @@ export interface ExportedContact {
phone: string;
}
export async function exportContacts(): Promise<
ApiResult<ExportedContact[]>
> {
export async function exportContacts(): Promise<ApiResult<ExportedContact[]>> {
const client = mtprotoService.getClient();
const result = await mtprotoService.withFloodWaitHandling(async () => {
@@ -2,14 +2,14 @@
* API: Forward message
*/
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { getChatById } from "./helpers";
import type { ApiResult } from "./types";
export async function forwardMessage(
fromChatId: string | number,
toChatId: string | number,
messageId: number,
messageId: number
): Promise<ApiResult<void>> {
const fromChat = getChatById(fromChatId);
if (!fromChat) {
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import bigInt from "big-integer";
@@ -13,7 +13,9 @@ export interface Admin {
username?: string;
}
export async function getAdmins(chatId: string | number): Promise<ApiResult<Admin[]>> {
export async function getAdmins(
chatId: string | number
): Promise<ApiResult<Admin[]>> {
const chat = getChatById(chatId);
if (!chat) {
throw new Error(`Chat not found: ${chatId}`);
@@ -34,7 +36,7 @@ export async function getAdmins(chatId: string | number): Promise<ApiResult<Admi
offset: 0,
limit: 100,
hash: bigInt(0),
}),
})
);
});
if (result && "users" in result && Array.isArray(result.users)) {
@@ -43,7 +45,7 @@ export async function getAdmins(chatId: string | number): Promise<ApiResult<Admi
} else {
const result = await mtprotoService.withFloodWaitHandling(async () => {
return client.invoke(
new Api.messages.GetFullChat({ chatId: bigInt(chat.id) }),
new Api.messages.GetFullChat({ chatId: bigInt(chat.id) })
);
});
if (result && "users" in result && Array.isArray(result.users)) {
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import bigInt from "big-integer";
@@ -15,7 +15,7 @@ export interface BannedUser {
export async function getBannedUsers(
chatId: string | number,
limit: number = 50,
limit: number = 50
): Promise<ApiResult<BannedUser[]>> {
const chat = getChatById(chatId);
if (!chat) {
@@ -24,7 +24,7 @@ export async function getBannedUsers(
if (chat.type !== "channel" && chat.type !== "supergroup") {
throw new Error(
"Banned users list is only available for channels/supergroups.",
"Banned users list is only available for channels/supergroups."
);
}
@@ -40,7 +40,7 @@ export async function getBannedUsers(
offset: 0,
limit,
hash: bigInt(0),
}),
})
);
});
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import type { ApiUser } from "./apiResultTypes";
@@ -11,7 +11,7 @@ export interface BlockedUser {
}
export async function getBlockedUsers(
limit: number = 50,
limit: number = 50
): Promise<ApiResult<BlockedUser[]>> {
const client = mtprotoService.getClient();
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import type { FullUserResult } from "./apiResultTypes";
@@ -16,14 +16,14 @@ export interface BotInfo {
}
export async function getBotInfo(
botId: string | number,
botId: string | number
): Promise<ApiResult<BotInfo>> {
const client = mtprotoService.getClient();
const result = await mtprotoService.withFloodWaitHandling(async () => {
const inputUser = await client.getInputEntity(botId);
return client.invoke(
new Api.users.GetFullUser({ id: toInputUser(inputUser) }),
new Api.users.GetFullUser({ id: toInputUser(inputUser) })
);
});
@@ -2,11 +2,11 @@
* API: Get chat cache-first with API fallback (hybrid).
*/
import { mtprotoService } from "../../../../services/mtprotoService";
import { enforceRateLimit } from "../../rateLimiter";
import { mtprotoService } from "../../../services/mtprotoService";
import { enforceRateLimit } from "../../mcp/rateLimiter";
import { getChatById, formatEntity } from "./helpers";
import type { ApiResult } from "./types";
import type { TelegramChat } from "../../../../store/telegram/types";
import type { TelegramChat } from "../../../store/telegram/types";
import { updateChatInState } from "../state";
export interface ChatInfo {
@@ -26,7 +26,7 @@ export interface ChatInfo {
}
export async function getChat(
chatId: string | number,
chatId: string | number
): Promise<ApiResult<ChatInfo | undefined>> {
// 1. Try cache
const chat = getChatById(chatId);
@@ -111,10 +111,10 @@ export async function getChat(
className === "User"
? "private"
: className === "Channel"
? raw.megagroup
? "supergroup"
: "channel"
: "group";
? raw.megagroup
? "supergroup"
: "channel"
: "group";
updateChatInState({
id: info.id,
title: info.name,
@@ -2,19 +2,17 @@
* API: Get chats cache-first with API fallback.
*/
import type { TelegramChat } from "../../../../store/telegram/types";
import { mtprotoService } from "../../../../services/mtprotoService";
import type { TelegramChat } from "../../../store/telegram/types";
import { mtprotoService } from "../../../services/mtprotoService";
import { Api } from "telegram";
import bigInt from "big-integer";
import { enforceRateLimit } from "../../rateLimiter";
import { enforceRateLimit } from "../../mcp/rateLimiter";
import { getOrderedChats, apiDialogToTelegramChat } from "./helpers";
import type { ApiResult } from "./types";
import { updateChatsInState, updateUsersFromApiUsers } from "../state";
import type { ApiUser } from "./apiResultTypes";
export async function getChats(
limit = 20,
): Promise<ApiResult<TelegramChat[]>> {
export async function getChats(limit = 20): Promise<ApiResult<TelegramChat[]>> {
// 1. Try cache
const cached = getOrderedChats(limit);
if (cached.length > 0) return { data: cached, fromCache: true };
@@ -38,7 +36,7 @@ export async function getChats(
offsetPeer: new Api.InputPeerEmpty(),
limit,
hash: bigInt(0),
}),
})
);
});
@@ -2,8 +2,8 @@
* API: Get contact chats cache-only.
*/
import { store } from "../../../../store";
import { selectOrderedChats } from "../../../../store/telegramSelectors";
import { store } from "../../../store";
import { selectOrderedChats } from "../../../store/telegramSelectors";
import type { ApiResult } from "./types";
export interface ContactChat {
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import bigInt from "big-integer";
@@ -19,7 +19,7 @@ export async function getContactIds(): Promise<ApiResult<string[]>> {
}
const ids = narrow<ContactIdResult[]>(result).map((c) =>
String(typeof c === "number" ? c : (c.userId ?? c)),
String(typeof c === "number" ? c : c.userId ?? c)
);
return { data: ids, fromCache: false };
@@ -2,9 +2,9 @@
* API: Get current user cache-first with API fallback.
*/
import type { TelegramUser } from "../../../../store/telegram/types";
import { mtprotoService } from "../../../../services/mtprotoService";
import { enforceRateLimit } from "../../rateLimiter";
import type { TelegramUser } from "../../../store/telegram/types";
import { mtprotoService } from "../../../services/mtprotoService";
import { enforceRateLimit } from "../../mcp/rateLimiter";
import { getCurrentUser as getCachedCurrentUser } from "./helpers";
import type { ApiResult } from "./types";
import { updateCurrentUserInState } from "../state";
@@ -2,8 +2,8 @@
* API: Get direct chat by contact cache-only.
*/
import { store } from "../../../../store";
import { selectOrderedChats } from "../../../../store/telegramSelectors";
import { store } from "../../../store";
import { selectOrderedChats } from "../../../store/telegramSelectors";
import type { ApiResult } from "./types";
export interface DirectChat {
@@ -13,13 +13,13 @@ export interface DirectChat {
}
export async function getDirectChatByContact(
userId: string | number,
userId: string | number
): Promise<ApiResult<DirectChat | undefined>> {
const state = store.getState();
const chats = selectOrderedChats(state);
const dmChat = chats.find(
(c) => c.type === "private" && String(c.id) === String(userId),
(c) => c.type === "private" && String(c.id) === String(userId)
);
if (!dmChat) return { data: undefined, fromCache: true };
@@ -2,7 +2,7 @@
* API: Get all drafts
*/
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import type { UpdatesResult } from "./apiResultTypes";
import { Api } from "telegram";
@@ -4,7 +4,7 @@
* Uses the @gif bot to search for GIFs.
*/
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import type { InlineBotResults } from "./apiResultTypes";
import { toInputUser, narrow } from "./apiCastHelpers";
@@ -17,7 +17,7 @@ export interface GifResult {
export async function getGifSearch(
query: string,
limit: number,
limit: number
): Promise<ApiResult<GifResult[]>> {
const client = mtprotoService.getClient();
@@ -29,7 +29,7 @@ export async function getGifSearch(
peer: new Api.InputPeerSelf(),
query,
offset: "",
}),
})
);
});
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { getChatById } from "./helpers";
import type { ApiResult } from "./types";
import { Api } from "telegram";
@@ -6,7 +6,7 @@ import type { ChatInviteResult } from "./apiResultTypes";
import { narrow } from "./apiCastHelpers";
export async function getInviteLink(
chatId: string | number,
chatId: string | number
): Promise<ApiResult<{ link: string }>> {
const chat = getChatById(chatId);
if (!chat) throw new Error(`Chat not found: ${chatId}`);
@@ -20,7 +20,7 @@ export async function getInviteLink(
new Api.messages.ExportChatInvite({
peer: inputPeer,
legacyRevokePermanent: true,
}),
})
);
});
@@ -2,7 +2,7 @@
* API: Get message reactions
*/
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { getChatById } from "./helpers";
import type { ApiResult } from "./types";
import { updateMessageReactionsInState } from "../state";
@@ -17,7 +17,7 @@ export interface ReactionData {
export async function getMessageReactions(
chatId: string | number,
messageId: number,
messageId: number
): Promise<ApiResult<ReactionData[]>> {
const chat = getChatById(chatId);
if (!chat) {
@@ -25,9 +25,7 @@ export async function getMessageReactions(
}
const client = mtprotoService.getClient();
const entity = chat.username
? `@${chat.username.replace("@", "")}`
: chat.id;
const entity = chat.username ? `@${chat.username.replace("@", "")}` : chat.id;
const result = await mtprotoService.withFloodWaitHandling(async () => {
const inputPeer = await client.getInputEntity(entity);
@@ -35,7 +33,7 @@ export async function getMessageReactions(
new Api.messages.GetMessagesReactions({
peer: inputPeer,
id: [messageId],
}),
})
);
});
@@ -60,7 +58,7 @@ export async function getMessageReactions(
updateMessageReactionsInState(
chat.id,
String(messageId),
reactions.map((r) => ({ emoticon: r.emoji, count: r.count })),
reactions.map((r) => ({ emoticon: r.emoji, count: r.count }))
);
}
@@ -2,11 +2,11 @@
* API: Get messages cache-first with API fallback.
*/
import type { TelegramMessage } from "../../../../store/telegram/types";
import { mtprotoService } from "../../../../services/mtprotoService";
import type { TelegramMessage } from "../../../store/telegram/types";
import { mtprotoService } from "../../../services/mtprotoService";
import { Api } from "telegram";
import bigInt from "big-integer";
import { enforceRateLimit } from "../../rateLimiter";
import { enforceRateLimit } from "../../mcp/rateLimiter";
import {
getChatById,
getCachedMessages,
@@ -19,7 +19,7 @@ import type { ApiUser } from "./apiResultTypes";
export async function getMessages(
chatId: string | number,
limit = 20,
offset = 0,
offset = 0
): Promise<ApiResult<TelegramMessage[]>> {
// 1. Try cache
const cached = getCachedMessages(chatId, limit, offset);
@@ -55,14 +55,12 @@ export async function getMessages(
maxId: 0,
minId: 0,
hash: bigInt(0),
}),
})
);
});
if ("messages" in result && Array.isArray(result.messages)) {
const messages = (
result.messages as unknown as Record<string, unknown>[]
)
const messages = (result.messages as unknown as Record<string, unknown>[])
.filter((m) => m.className !== "MessageEmpty")
.map((m) => apiMessageToTelegramMessage(m, chat.id));
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import bigInt from "big-integer";
@@ -15,7 +15,7 @@ export interface Participant {
export async function getParticipants(
chatId: string | number,
limit: number = 50,
limit: number = 50
): Promise<ApiResult<Participant[]>> {
const chat = getChatById(chatId);
if (!chat) {
@@ -37,7 +37,7 @@ export async function getParticipants(
offset: 0,
limit,
hash: bigInt(0),
}),
})
);
});
if (result && "users" in result && Array.isArray(result.users)) {
@@ -46,7 +46,7 @@ export async function getParticipants(
} else {
const result = await mtprotoService.withFloodWaitHandling(async () => {
return client.invoke(
new Api.messages.GetFullChat({ chatId: bigInt(chat.id) }),
new Api.messages.GetFullChat({ chatId: bigInt(chat.id) })
);
});
if (result && "users" in result && Array.isArray(result.users)) {
@@ -2,7 +2,7 @@
* API: Get pinned messages (API-first with cache fallback)
*/
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { getChatById } from "./helpers";
import type { ApiResult } from "./types";
import type { ApiMessage } from "./apiResultTypes";
@@ -17,16 +17,14 @@ export interface PinnedMessage {
}
export async function getPinnedMessages(
chatId: string | number,
chatId: string | number
): Promise<ApiResult<PinnedMessage[]>> {
const chat = getChatById(chatId);
if (!chat) {
throw new Error(`Chat not found: ${chatId}`);
}
const entity = chat.username
? `@${chat.username.replace("@", "")}`
: chat.id;
const entity = chat.username ? `@${chat.username.replace("@", "")}` : chat.id;
const client = mtprotoService.getClient();
// Try API first
@@ -45,17 +43,17 @@ export async function getPinnedMessages(
maxId: 0,
minId: 0,
hash: bigInt(0),
}),
})
);
if ("messages" in result && Array.isArray(result.messages)) {
const pinnedMessages = narrow<ApiMessage[]>(result.messages).map((msg) => ({
id: msg.id ?? "?",
date: msg.date
? new Date(msg.date * 1000).toISOString()
: "unknown",
text: msg.message ?? "[Media/No text]",
}));
const pinnedMessages = narrow<ApiMessage[]>(result.messages).map(
(msg) => ({
id: msg.id ?? "?",
date: msg.date ? new Date(msg.date * 1000).toISOString() : "unknown",
text: msg.message ?? "[Media/No text]",
})
);
return { data: pinnedMessages, fromCache: false };
}
} catch {
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import type { PrivacyResult } from "./apiResultTypes";
@@ -27,14 +27,14 @@ const keyMap: Record<PrivacyKey, Api.TypeInputPrivacyKey> = {
};
export async function getPrivacySettings(
key: string = "last_seen",
key: string = "last_seen"
): Promise<ApiResult<PrivacySettings>> {
if (!(key in keyMap)) {
throw new Error(
"Unknown privacy key: " +
key +
". Valid keys: " +
Object.keys(keyMap).join(", "),
Object.keys(keyMap).join(", ")
);
}
@@ -42,7 +42,9 @@ export async function getPrivacySettings(
const client = mtprotoService.getClient();
const result = await mtprotoService.withFloodWaitHandling(async () => {
return client.invoke(new Api.account.GetPrivacy({ key: keyMap[privacyKey] }));
return client.invoke(
new Api.account.GetPrivacy({ key: keyMap[privacyKey] })
);
});
const rules = narrow<PrivacyResult>(result)?.rules;
@@ -4,7 +4,7 @@
* Fetches admin log from channels/supergroups only.
*/
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { getChatById } from "./helpers";
import type { ApiResult } from "./types";
import type { AdminLogResult } from "./apiResultTypes";
@@ -19,7 +19,7 @@ export interface AdminActionResult {
export async function getRecentActions(
chatId: string | number,
limit: number,
limit: number
): Promise<ApiResult<AdminActionResult[]>> {
const chat = getChatById(chatId);
if (!chat) {
@@ -28,7 +28,7 @@ export async function getRecentActions(
if (chat.type !== "channel" && chat.type !== "supergroup") {
throw new Error(
"Recent actions are only available for channels/supergroups.",
"Recent actions are only available for channels/supergroups."
);
}
@@ -44,7 +44,7 @@ export async function getRecentActions(
maxId: bigInt(0),
minId: bigInt(0),
limit,
}),
})
);
});
@@ -4,7 +4,7 @@
* Fetches all available sticker sets for the current user.
*/
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import type { StickerSetsResult } from "./apiResultTypes";
import { narrow } from "./apiCastHelpers";
@@ -17,9 +17,7 @@ export interface StickerSetResult {
count: number;
}
export async function getStickerSets(): Promise<
ApiResult<StickerSetResult[]>
> {
export async function getStickerSets(): Promise<ApiResult<StickerSetResult[]>> {
const client = mtprotoService.getClient();
const result = await mtprotoService.withFloodWaitHandling(async () => {
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import bigInt from "big-integer";
@@ -12,7 +12,7 @@ export interface UserPhoto {
export async function getUserPhotos(
userId: string | number,
limit: number = 10,
limit: number = 10
): Promise<ApiResult<UserPhoto[]>> {
const client = mtprotoService.getClient();
@@ -24,7 +24,7 @@ export async function getUserPhotos(
offset: 0,
maxId: bigInt(0),
limit,
}),
})
);
});
@@ -39,9 +39,7 @@ export async function getUserPhotos(
const photos = narrow<ApiPhoto[]>(result.photos).map((photo) => ({
id: String(photo.id),
date: photo.date
? new Date(photo.date * 1000).toISOString()
: "unknown",
date: photo.date ? new Date(photo.date * 1000).toISOString() : "unknown",
}));
return { data: photos, fromCache: false };
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import type { ApiUser } from "./apiResultTypes";
@@ -13,14 +13,14 @@ export interface UserStatus {
}
export async function getUserStatus(
userId: string | number,
userId: string | number
): Promise<ApiResult<UserStatus>> {
const client = mtprotoService.getClient();
const result = await mtprotoService.withFloodWaitHandling(async () => {
const inputUser = await client.getInputEntity(userId);
return client.invoke(
new Api.users.GetUsers({ id: [toInputUser(inputUser)] }),
new Api.users.GetUsers({ id: [toInputUser(inputUser)] })
);
});
@@ -5,17 +5,17 @@
* These are used by both api/ functions and (transitionally) by tool wrappers.
*/
import { store } from "../../../../store";
import { store } from "../../../store";
import {
selectOrderedChats,
selectCurrentUser,
selectTelegramUserState,
} from "../../../../store/telegramSelectors";
} from "../../../store/telegramSelectors";
import type {
TelegramChat,
TelegramUser,
TelegramMessage,
} from "../../../../store/telegram/types";
} from "../../../store/telegram/types";
// ---------------------------------------------------------------------------
// Redux state helpers
@@ -32,9 +32,7 @@ export function getTelegramState() {
/**
* Get chat by ID or username from Redux cache.
*/
export function getChatById(
chatId: string | number,
): TelegramChat | undefined {
export function getChatById(chatId: string | number): TelegramChat | undefined {
const state = getTelegramState();
const idStr = String(chatId);
@@ -49,7 +47,7 @@ export function getChatById(
return Object.values(state.chats).find(
(c) =>
c.username &&
(c.username === username || c.username === username.slice(1)),
(c.username === username || c.username === username.slice(1))
);
}
@@ -59,9 +57,7 @@ export function getChatById(
/**
* Get user by ID (current user only; no full user cache).
*/
export function getUserById(
userId: string | number,
): TelegramUser | undefined {
export function getUserById(userId: string | number): TelegramUser | undefined {
const state = getTelegramState();
const current = state.currentUser;
if (!current) return undefined;
@@ -92,7 +88,7 @@ export function getOrderedChats(limit = 20): TelegramChat[] {
export function getCachedMessages(
chatId: string | number,
limit = 20,
offset = 0,
offset = 0
): TelegramMessage[] | undefined {
const chat = getChatById(chatId);
if (!chat) return undefined;
@@ -145,7 +141,7 @@ export interface FormattedMessage {
* Format entity (chat or user) for display.
*/
export function formatEntity(
entity: TelegramChat | TelegramUser,
entity: TelegramChat | TelegramUser
): FormattedEntity {
if ("title" in entity) {
const chat = entity as TelegramChat;
@@ -153,8 +149,8 @@ export function formatEntity(
chat.type === "channel"
? "channel"
: chat.type === "supergroup"
? "group"
: chat.type;
? "group"
: chat.type;
return {
id: chat.id,
name: chat.title ?? "Unknown",
@@ -198,7 +194,7 @@ export function formatMessage(message: TelegramMessage): FormattedMessage {
/** Convert a raw GramJS message to our TelegramMessage format */
export function apiMessageToTelegramMessage(
msg: Record<string, unknown>,
chatId: string,
chatId: string
): TelegramMessage {
const fromId =
msg.fromId &&
@@ -225,9 +221,7 @@ export function apiMessageToTelegramMessage(
isEdited: msg.editDate != null,
isForwarded: msg.fwdFrom != null,
replyToMessageId:
replyTo?.replyToMsgId != null
? String(replyTo.replyToMsgId)
: undefined,
replyTo?.replyToMsgId != null ? String(replyTo.replyToMsgId) : undefined,
media: mediaInfo,
};
}
@@ -236,7 +230,7 @@ export function apiMessageToTelegramMessage(
export function apiDialogToTelegramChat(
dialog: Record<string, unknown>,
chatsById: Map<string, Record<string, unknown>>,
usersById: Map<string, Record<string, unknown>>,
usersById: Map<string, Record<string, unknown>>
): TelegramChat | undefined {
const peer = dialog.peer as
| {
@@ -1,11 +1,11 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import type { ResultWithChats } from "./apiResultTypes";
import { narrow } from "./apiCastHelpers";
export async function importChatInvite(
hash: string,
hash: string
): Promise<ApiResult<{ chatTitle: string }>> {
const client = mtprotoService.getClient();
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import bigInt from "big-integer";
@@ -11,7 +11,7 @@ export interface ImportContactsResponse {
}
export async function importContacts(
contacts: ContactInput[],
contacts: ContactInput[]
): Promise<ApiResult<ImportContactsResponse>> {
if (!Array.isArray(contacts) || contacts.length === 0) {
throw new Error("contacts array is required and must not be empty.");
@@ -24,14 +24,14 @@ export async function importContacts(
phone: String(c.phone ?? ""),
firstName: String(c.first_name ?? ""),
lastName: String(c.last_name ?? ""),
}),
})
);
const client = mtprotoService.getClient();
const result = await mtprotoService.withFloodWaitHandling(async () => {
return client.invoke(
new Api.contacts.ImportContacts({ contacts: inputContacts }),
new Api.contacts.ImportContacts({ contacts: inputContacts })
);
});
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import bigInt from "big-integer";
@@ -7,7 +7,7 @@ import { getChatById } from "./helpers";
export async function inviteToGroup(
chatId: string | number,
userIds: string[],
userIds: string[]
): Promise<ApiResult<void>> {
if (userIds.length === 0) {
throw new Error("user_ids must not be empty");
@@ -35,7 +35,7 @@ export async function inviteToGroup(
new Api.channels.InviteToChannel({
channel: toInputChannel(inputPeer),
users,
}),
})
);
});
} else {
@@ -46,7 +46,7 @@ export async function inviteToGroup(
chatId: bigInt(chat.id),
userId: user,
fwdLimit: 100,
}),
})
);
});
}
@@ -1,11 +1,11 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import type { ResultWithChats } from "./apiResultTypes";
import { narrow } from "./apiCastHelpers";
export async function joinChatByLink(
link: string,
link: string
): Promise<ApiResult<{ chatTitle: string }>> {
// Extract hash from link
let hash = link;
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { getChatById } from "./helpers";
import type { ApiResult } from "./types";
import { Api } from "telegram";
@@ -6,7 +6,7 @@ import bigInt from "big-integer";
import { toInputChannel } from "./apiCastHelpers";
export async function leaveChat(
chatId: string | number,
chatId: string | number
): Promise<ApiResult<void>> {
const chat = getChatById(chatId);
if (!chat) throw new Error(`Chat not found: ${chatId}`);
@@ -20,7 +20,7 @@ export async function leaveChat(
await client.invoke(
new Api.channels.LeaveChannel({
channel: toInputChannel(inputChannel),
}),
})
);
});
} else {
@@ -29,7 +29,7 @@ export async function leaveChat(
new Api.messages.DeleteChatUser({
chatId: bigInt(chat.id),
userId: new Api.InputUserSelf(),
}),
})
);
});
}
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import bigInt from "big-integer";
@@ -4,7 +4,7 @@
* Fetches forum topics from channels/supergroups only.
*/
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { getChatById } from "./helpers";
import type { ApiResult } from "./types";
import type { ForumTopicsResult } from "./apiResultTypes";
@@ -17,7 +17,7 @@ export interface TopicResult {
}
export async function listTopics(
chatId: string | number,
chatId: string | number
): Promise<ApiResult<TopicResult[]>> {
const chat = getChatById(chatId);
if (!chat) {
@@ -26,7 +26,7 @@ export async function listTopics(
if (chat.type !== "channel" && chat.type !== "supergroup") {
throw new Error(
"Forum topics are only available for channels/supergroups.",
"Forum topics are only available for channels/supergroups."
);
}
@@ -42,7 +42,7 @@ export async function listTopics(
offsetId: 0,
offsetTopic: 0,
limit: 100,
}),
})
);
});
@@ -2,21 +2,19 @@
* API: Mark as read
*/
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { getChatById } from "./helpers";
import type { ApiResult } from "./types";
export async function markAsRead(
chatId: string | number,
chatId: string | number
): Promise<ApiResult<void>> {
const chat = getChatById(chatId);
if (!chat) {
throw new Error(`Chat not found: ${chatId}`);
}
const entity = chat.username
? `@${chat.username.replace("@", "")}`
: chat.id;
const entity = chat.username ? `@${chat.username.replace("@", "")}` : chat.id;
const client = mtprotoService.getClient();
await mtprotoService.withFloodWaitHandling(async () => {
@@ -1,11 +1,11 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { getChatById } from "./helpers";
import type { ApiResult } from "./types";
import { Api } from "telegram";
export async function muteChat(
chatId: string | number,
duration?: number,
duration?: number
): Promise<ApiResult<void>> {
const chat = getChatById(chatId);
if (!chat) throw new Error(`Chat not found: ${chatId}`);
@@ -26,7 +26,7 @@ export async function muteChat(
settings: new Api.InputPeerNotifySettings({
muteUntil,
}),
}),
})
);
});
@@ -2,22 +2,20 @@
* API: Pin message
*/
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { getChatById } from "./helpers";
import type { ApiResult } from "./types";
export async function pinMessage(
chatId: string | number,
messageId: number,
messageId: number
): Promise<ApiResult<void>> {
const chat = getChatById(chatId);
if (!chat) {
throw new Error(`Chat not found: ${chatId}`);
}
const entity = chat.username
? `@${chat.username.replace("@", "")}`
: chat.id;
const entity = chat.username ? `@${chat.username.replace("@", "")}` : chat.id;
const client = mtprotoService.getClient();
await mtprotoService.withFloodWaitHandling(async () => {
@@ -2,7 +2,7 @@
* API: Press inline button
*/
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { getChatById } from "./helpers";
import type { ApiResult } from "./types";
import type { BotCallbackAnswer } from "./apiResultTypes";
@@ -12,7 +12,7 @@ import { narrow } from "./apiCastHelpers";
export async function pressInlineButton(
chatId: string | number,
messageId: number,
buttonData: string,
buttonData: string
): Promise<ApiResult<string>> {
const chat = getChatById(chatId);
if (!chat) {
@@ -20,9 +20,7 @@ export async function pressInlineButton(
}
const client = mtprotoService.getClient();
const entity = chat.username
? `@${chat.username.replace("@", "")}`
: chat.id;
const entity = chat.username ? `@${chat.username.replace("@", "")}` : chat.id;
const result = await mtprotoService.withFloodWaitHandling(async () => {
const inputPeer = await client.getInputEntity(entity);
@@ -31,7 +29,7 @@ export async function pressInlineButton(
peer: inputPeer,
msgId: messageId,
data: Buffer.from(buttonData, "base64"),
}),
})
);
});
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import { toInputChannel, toInputUser } from "./apiCastHelpers";
@@ -6,7 +6,7 @@ import { getChatById } from "./helpers";
export async function promoteAdmin(
chatId: string | number,
userId: string | number,
userId: string | number
): Promise<ApiResult<void>> {
const chat = getChatById(chatId);
if (!chat) {
@@ -15,7 +15,7 @@ export async function promoteAdmin(
if (chat.type !== "channel" && chat.type !== "supergroup") {
throw new Error(
"Admin promotion is only available for channels/supergroups.",
"Admin promotion is only available for channels/supergroups."
);
}
@@ -38,7 +38,7 @@ export async function promoteAdmin(
manageCall: true,
}),
rank: "Admin",
}),
})
);
});
@@ -2,14 +2,14 @@
* API: Remove reaction
*/
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { getChatById } from "./helpers";
import type { ApiResult } from "./types";
import { Api } from "telegram";
export async function removeReaction(
chatId: string | number,
messageId: number,
messageId: number
): Promise<ApiResult<void>> {
const chat = getChatById(chatId);
if (!chat) {
@@ -17,9 +17,7 @@ export async function removeReaction(
}
const client = mtprotoService.getClient();
const entity = chat.username
? `@${chat.username.replace("@", "")}`
: chat.id;
const entity = chat.username ? `@${chat.username.replace("@", "")}` : chat.id;
await mtprotoService.withFloodWaitHandling(async () => {
const inputPeer = await client.getInputEntity(entity);
@@ -28,7 +26,7 @@ export async function removeReaction(
peer: inputPeer,
msgId: messageId,
reaction: [],
}),
})
);
});
@@ -2,7 +2,7 @@
* API: Resolve username API-first with cache fallback (hybrid).
*/
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { Api } from "telegram";
import { getChatById, formatEntity } from "./helpers";
import type { ApiResult } from "./types";
@@ -17,7 +17,7 @@ export interface ResolvedEntity {
}
export async function resolveUsername(
username: string,
username: string
): Promise<ApiResult<ResolvedEntity | undefined>> {
const clean = username.startsWith("@") ? username.slice(1) : username;
@@ -27,7 +27,7 @@ export async function resolveUsername(
const result = await mtprotoService.withFloodWaitHandling(async () => {
return client.invoke(
new Api.contacts.ResolveUsername({ username: clean }),
new Api.contacts.ResolveUsername({ username: clean })
);
});
@@ -37,19 +37,19 @@ export async function resolveUsername(
peer.className === "PeerUser"
? "user"
: peer.className === "PeerChannel"
? "channel"
: peer.className === "PeerChat"
? "chat"
: "unknown";
? "channel"
: peer.className === "PeerChat"
? "chat"
: "unknown";
const peerId =
"userId" in peer
? String(peer.userId)
: "channelId" in peer
? String(peer.channelId)
: "chatId" in peer
? String(peer.chatId)
: "unknown";
? String(peer.channelId)
: "chatId" in peer
? String(peer.chatId)
: "unknown";
let name = clean;
if ("users" in result && Array.isArray(result.users)) {
@@ -2,14 +2,14 @@
* API: Save draft
*/
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { getChatById } from "./helpers";
import type { ApiResult } from "./types";
import { Api } from "telegram";
export async function saveDraft(
chatId: string | number,
text: string,
text: string
): Promise<ApiResult<void>> {
const chat = getChatById(chatId);
if (!chat) {
@@ -17,9 +17,7 @@ export async function saveDraft(
}
const client = mtprotoService.getClient();
const entity = chat.username
? `@${chat.username.replace("@", "")}`
: chat.id;
const entity = chat.username ? `@${chat.username.replace("@", "")}` : chat.id;
await mtprotoService.withFloodWaitHandling(async () => {
const inputPeer = await client.getInputEntity(entity);
@@ -27,7 +25,7 @@ export async function saveDraft(
new Api.messages.SaveDraft({
peer: inputPeer,
message: text,
}),
})
);
});
@@ -2,12 +2,12 @@
* API: Search chats cache-only.
*/
import type { TelegramChat } from "../../../../store/telegram/types";
import type { TelegramChat } from "../../../store/telegram/types";
import { searchChatsInCache } from "./helpers";
import type { ApiResult } from "./types";
export async function searchChats(
query: string,
query: string
): Promise<ApiResult<TelegramChat[]>> {
const data = searchChatsInCache(query);
return { data, fromCache: true };
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import type { ApiUser } from "./apiResultTypes";
@@ -12,7 +12,7 @@ export interface ContactSearchResult {
export async function searchContacts(
query: string,
limit: number = 20,
limit: number = 20
): Promise<ApiResult<ContactSearchResult[]>> {
if (!query) {
throw new Error("query is required");
@@ -2,7 +2,7 @@
* API: Search messages (API-first with cache fallback)
*/
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { getChatById, formatMessage, getCachedMessages } from "./helpers";
import type { ApiResult } from "./types";
import type { ApiMessage } from "./apiResultTypes";
@@ -20,7 +20,7 @@ export interface SearchedMessage {
export async function searchMessages(
chatId: string | number,
query: string,
limit: number,
limit: number
): Promise<ApiResult<SearchedMessage[]>> {
const chat = getChatById(chatId);
if (!chat) {
@@ -49,7 +49,7 @@ export async function searchMessages(
maxId: 0,
minId: 0,
hash: bigInt(0),
}),
})
);
});
@@ -57,9 +57,7 @@ export async function searchMessages(
const messages = narrow<ApiMessage[]>(result.messages);
const searchedMessages = messages.map((msg) => ({
id: msg.id ?? "?",
date: msg.date
? new Date(msg.date * 1000).toISOString()
: "unknown",
date: msg.date ? new Date(msg.date * 1000).toISOString() : "unknown",
text: msg.message ?? "[Media/No text]",
}));
return { data: searchedMessages, fromCache: false };
@@ -5,7 +5,7 @@
* Falls back to filtering cached chats if the API call fails.
*/
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { searchChatsInCache } from "./helpers";
import type { ApiResult } from "./types";
import { Api } from "telegram";
@@ -20,7 +20,7 @@ export interface PublicChatResult {
}
export async function searchPublicChats(
query: string,
query: string
): Promise<ApiResult<PublicChatResult[]>> {
// Try server-side search via Telegram API
try {
@@ -42,11 +42,7 @@ export async function searchPublicChats(
megagroup?: boolean;
broadcast?: boolean;
};
const type = c.broadcast
? "channel"
: c.megagroup
? "group"
: "chat";
const type = c.broadcast ? "channel" : c.megagroup ? "group" : "chat";
entries.push({
id: String(c.id),
name: c.title ?? "Unknown",
@@ -2,21 +2,19 @@
* API: Send message always API (write operation).
*/
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { getChatById } from "./helpers";
import type { ApiResult } from "./types";
export async function sendMessage(
chatId: string | number,
message: string,
replyToMessageId?: number,
replyToMessageId?: number
): Promise<ApiResult<{ id: string } | undefined>> {
const chat = getChatById(chatId);
if (!chat) return { data: undefined, fromCache: false };
const entity = chat.username
? `@${chat.username.replace("@", "")}`
: chat.id;
const entity = chat.username ? `@${chat.username.replace("@", "")}` : chat.id;
if (replyToMessageId !== undefined) {
const client = mtprotoService.getClient();
@@ -2,7 +2,7 @@
* API: Send reaction
*/
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { getChatById } from "./helpers";
import type { ApiResult } from "./types";
import { Api } from "telegram";
@@ -10,7 +10,7 @@ import { Api } from "telegram";
export async function sendReaction(
chatId: string | number,
messageId: number,
emoji: string,
emoji: string
): Promise<ApiResult<void>> {
const chat = getChatById(chatId);
if (!chat) {
@@ -18,9 +18,7 @@ export async function sendReaction(
}
const client = mtprotoService.getClient();
const entity = chat.username
? `@${chat.username.replace("@", "")}`
: chat.id;
const entity = chat.username ? `@${chat.username.replace("@", "")}` : chat.id;
await mtprotoService.withFloodWaitHandling(async () => {
const inputPeer = await client.getInputEntity(entity);
@@ -29,7 +27,7 @@ export async function sendReaction(
peer: inputPeer,
msgId: messageId,
reaction: [new Api.ReactionEmoji({ emoticon: emoji })],
}),
})
);
});
@@ -1,10 +1,10 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import type { BotCommandInput } from "./apiResultTypes";
export async function setBotCommands(
commands: BotCommandInput[],
commands: BotCommandInput[]
): Promise<ApiResult<number>> {
if (!Array.isArray(commands) || commands.length === 0) {
throw new Error("commands array is required and must not be empty");
@@ -17,7 +17,7 @@ export async function setBotCommands(
new Api.BotCommand({
command: String(c.command ?? ""),
description: String(c.description ?? ""),
}),
})
);
await mtprotoService.withFloodWaitHandling(async () => {
@@ -26,7 +26,7 @@ export async function setBotCommands(
scope: new Api.BotCommandScopeDefault(),
langCode: "",
commands: botCommands,
}),
})
);
});
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
@@ -29,7 +29,7 @@ const ruleMap: Record<PrivacyRule, Api.TypeInputPrivacyRule> = {
export async function setPrivacySettings(
setting: string,
value: string,
value: string
): Promise<ApiResult<void>> {
if (!setting) {
throw new Error("setting is required");
@@ -46,7 +46,7 @@ export async function setPrivacySettings(
throw new Error(
"Unknown rule: " +
value +
". Valid: allow_all, allow_contacts, disallow_all",
". Valid: allow_all, allow_contacts, disallow_all"
);
}
@@ -60,7 +60,7 @@ export async function setPrivacySettings(
new Api.account.SetPrivacy({
key: keyMap[privacyKey],
rules: [ruleMap[privacyRule]],
}),
})
);
});
@@ -1,10 +1,10 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import { toInputChannel } from "./apiCastHelpers";
export async function subscribePublicChannel(
username: string,
username: string
): Promise<ApiResult<void>> {
const client = mtprotoService.getClient();
@@ -13,7 +13,7 @@ export async function subscribePublicChannel(
await client.invoke(
new Api.channels.JoinChannel({
channel: toInputChannel(inputChannel),
}),
})
);
});
@@ -1,10 +1,10 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { getChatById } from "./helpers";
import type { ApiResult } from "./types";
import { Api } from "telegram";
export async function unarchiveChat(
chatId: string | number,
chatId: string | number
): Promise<ApiResult<void>> {
const chat = getChatById(chatId);
if (!chat) throw new Error(`Chat not found: ${chatId}`);
@@ -22,7 +22,7 @@ export async function unarchiveChat(
folderId: 0, // 0 = Main folder (unarchive)
}),
],
}),
})
);
});
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import { toInputChannel, toInputPeer } from "./apiCastHelpers";
@@ -6,7 +6,7 @@ import { getChatById } from "./helpers";
export async function unbanUser(
chatId: string | number,
userId: string | number,
userId: string | number
): Promise<ApiResult<void>> {
const chat = getChatById(chatId);
if (!chat) {
@@ -28,7 +28,7 @@ export async function unbanUser(
channel: toInputChannel(inputChannel),
participant: toInputPeer(inputUser),
bannedRights: new Api.ChatBannedRights({ untilDate: 0 }),
}),
})
);
});
@@ -1,15 +1,17 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
import { toInputPeer } from "./apiCastHelpers";
export async function unblockUser(userId: string | number): Promise<ApiResult<void>> {
export async function unblockUser(
userId: string | number
): Promise<ApiResult<void>> {
const client = mtprotoService.getClient();
await mtprotoService.withFloodWaitHandling(async () => {
const inputUser = await client.getInputEntity(userId);
await client.invoke(
new Api.contacts.Unblock({ id: toInputPeer(inputUser) }),
new Api.contacts.Unblock({ id: toInputPeer(inputUser) })
);
});
@@ -1,10 +1,10 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { getChatById } from "./helpers";
import type { ApiResult } from "./types";
import { Api } from "telegram";
export async function unmuteChat(
chatId: string | number,
chatId: string | number
): Promise<ApiResult<void>> {
const chat = getChatById(chatId);
if (!chat) throw new Error(`Chat not found: ${chatId}`);
@@ -20,7 +20,7 @@ export async function unmuteChat(
settings: new Api.InputPeerNotifySettings({
muteUntil: 0,
}),
}),
})
);
});
@@ -2,23 +2,21 @@
* API: Unpin message
*/
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import { getChatById } from "./helpers";
import type { ApiResult } from "./types";
import { Api } from "telegram";
export async function unpinMessage(
chatId: string | number,
messageId: number,
messageId: number
): Promise<ApiResult<void>> {
const chat = getChatById(chatId);
if (!chat) {
throw new Error(`Chat not found: ${chatId}`);
}
const entity = chat.username
? `@${chat.username.replace("@", "")}`
: chat.id;
const entity = chat.username ? `@${chat.username.replace("@", "")}` : chat.id;
const client = mtprotoService.getClient();
await mtprotoService.withFloodWaitHandling(async () => {
@@ -28,7 +26,7 @@ export async function unpinMessage(
peer: inputPeer,
id: messageId,
unpin: true,
}),
})
);
});
@@ -1,4 +1,4 @@
import { mtprotoService } from "../../../../services/mtprotoService";
import { mtprotoService } from "../../../services/mtprotoService";
import type { ApiResult } from "./types";
import { Api } from "telegram";
@@ -9,15 +9,11 @@ export interface ProfileUpdate {
}
export async function updateProfile(
update: ProfileUpdate,
update: ProfileUpdate
): Promise<ApiResult<string[]>> {
if (
!update.firstName &&
!update.lastName &&
!update.about
) {
if (!update.firstName && !update.lastName && !update.about) {
throw new Error(
"At least one of firstName, lastName, or about is required.",
"At least one of firstName, lastName, or about is required."
);
}
@@ -29,7 +25,7 @@ export async function updateProfile(
firstName: update.firstName ?? undefined,
lastName: update.lastName ?? undefined,
about: update.about ?? undefined,
}),
})
);
});
@@ -1,7 +1,7 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { getChats, tool } from "../getChats";
import type { TelegramMCPContext } from "../../types";
import type { TelegramState } from "../../../../../store/telegram/types";
import type { TelegramState } from "../../../../store/telegram/types";
vi.mock("../../api/getChats");
@@ -27,9 +27,27 @@ describe("getChats tool handler", () => {
it("should return formatted chat list with IDs and titles", async () => {
const mockChats = [
{ id: "123", title: "Tech Group", type: "group" as const, unreadCount: 0, isPinned: false },
{ id: "456", title: "Trading Chat", type: "group" as const, unreadCount: 0, isPinned: false },
{ id: "789", title: "John Doe", type: "private" as const, unreadCount: 0, isPinned: false },
{
id: "123",
title: "Tech Group",
type: "group" as const,
unreadCount: 0,
isPinned: false,
},
{
id: "456",
title: "Trading Chat",
type: "group" as const,
unreadCount: 0,
isPinned: false,
},
{
id: "789",
title: "John Doe",
type: "private" as const,
unreadCount: 0,
isPinned: false,
},
];
vi.mocked(getChatsApi).mockResolvedValue({
@@ -62,8 +80,20 @@ describe("getChats tool handler", () => {
it("should return error for empty page", async () => {
const mockChats = [
{ id: "123", title: "Chat 1", type: "private" as const, unreadCount: 0, isPinned: false },
{ id: "456", title: "Chat 2", type: "private" as const, unreadCount: 0, isPinned: false },
{
id: "123",
title: "Chat 1",
type: "private" as const,
unreadCount: 0,
isPinned: false,
},
{
id: "456",
title: "Chat 2",
type: "private" as const,
unreadCount: 0,
isPinned: false,
},
];
vi.mocked(getChatsApi).mockResolvedValue({
@@ -71,21 +101,48 @@ describe("getChats tool handler", () => {
fromCache: false,
});
const result = await getChats(
{ page: 5, page_size: 10 },
mockContext
);
const result = await getChats({ page: 5, page_size: 10 }, mockContext);
expect(result.content[0].text).toContain("Page out of range");
});
it("should handle pagination with page 2 and page_size 2", async () => {
const mockChats = [
{ id: "1", title: "Chat 1", type: "private" as const, unreadCount: 0, isPinned: false },
{ id: "2", title: "Chat 2", type: "private" as const, unreadCount: 0, isPinned: false },
{ id: "3", title: "Chat 3", type: "private" as const, unreadCount: 0, isPinned: false },
{ id: "4", title: "Chat 4", type: "private" as const, unreadCount: 0, isPinned: false },
{ id: "5", title: "Chat 5", type: "private" as const, unreadCount: 0, isPinned: false },
{
id: "1",
title: "Chat 1",
type: "private" as const,
unreadCount: 0,
isPinned: false,
},
{
id: "2",
title: "Chat 2",
type: "private" as const,
unreadCount: 0,
isPinned: false,
},
{
id: "3",
title: "Chat 3",
type: "private" as const,
unreadCount: 0,
isPinned: false,
},
{
id: "4",
title: "Chat 4",
type: "private" as const,
unreadCount: 0,
isPinned: false,
},
{
id: "5",
title: "Chat 5",
type: "private" as const,
unreadCount: 0,
isPinned: false,
},
];
vi.mocked(getChatsApi).mockResolvedValue({
@@ -93,10 +150,7 @@ describe("getChats tool handler", () => {
fromCache: false,
});
const result = await getChats(
{ page: 2, page_size: 2 },
mockContext
);
const result = await getChats({ page: 2, page_size: 2 }, mockContext);
expect(result.isError).toBeFalsy();
const text = result.content[0].text;
@@ -1,7 +1,7 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { getMe, tool } from "../getMe";
import type { TelegramMCPContext } from "../../types";
import type { TelegramState } from "../../../../../store/telegram/types";
import type { TelegramState } from "../../../../store/telegram/types";
vi.mock("../../api/getCurrentUser");
@@ -1,7 +1,7 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { getMessages, tool } from "../getMessages";
import type { TelegramMCPContext } from "../../types";
import type { TelegramState } from "../../../../../store/telegram/types";
import type { TelegramState } from "../../../../store/telegram/types";
vi.mock("../../api/getMessages");
vi.mock("../../api/helpers", async (importOriginal) => {
@@ -44,10 +44,7 @@ describe("getMessages tool handler", () => {
it("should return error when chat is not found", async () => {
vi.mocked(getChatById).mockReturnValue(undefined);
const result = await getMessages(
{ chat_id: "123456789" },
mockContext
);
const result = await getMessages({ chat_id: "123456789" }, mockContext);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("Chat not found: 123456789");
@@ -90,10 +87,7 @@ describe("getMessages tool handler", () => {
fromCache: false,
});
const result = await getMessages(
{ chat_id: "123456789" },
mockContext
);
const result = await getMessages({ chat_id: "123456789" }, mockContext);
expect(result.isError).toBeFalsy();
const text = result.content[0].text;
@@ -117,10 +111,7 @@ describe("getMessages tool handler", () => {
fromCache: false,
});
const result = await getMessages(
{ chat_id: "123456789" },
mockContext
);
const result = await getMessages({ chat_id: "123456789" }, mockContext);
expect(result.isError).toBeFalsy();
expect(result.content[0].text).toContain("No messages found for this page");
@@ -133,8 +124,26 @@ describe("getMessages tool handler", () => {
};
const mockMessages = [
{ id: "3", chatId: "123456789", fromId: "user3", message: "Message 3", date: 1609459300, isOutgoing: false, isEdited: false, isForwarded: false },
{ id: "4", chatId: "123456789", fromId: "user4", message: "Message 4", date: 1609459400, isOutgoing: false, isEdited: false, isForwarded: false },
{
id: "3",
chatId: "123456789",
fromId: "user3",
message: "Message 3",
date: 1609459300,
isOutgoing: false,
isEdited: false,
isForwarded: false,
},
{
id: "4",
chatId: "123456789",
fromId: "user4",
message: "Message 4",
date: 1609459400,
isOutgoing: false,
isEdited: false,
isForwarded: false,
},
];
vi.mocked(getChatById).mockReturnValue(mockChat as any);
@@ -150,11 +159,7 @@ describe("getMessages tool handler", () => {
expect(result.isError).toBeFalsy();
// validateId converts string to number
expect(getMessagesApi).toHaveBeenCalledWith(
123456789,
2,
2
);
expect(getMessagesApi).toHaveBeenCalledWith(123456789, 2, 2);
});
it("should use default pagination values", async () => {
@@ -172,11 +177,7 @@ describe("getMessages tool handler", () => {
await getMessages({ chat_id: "123456789" }, mockContext);
// validateId converts string to number
expect(getMessagesApi).toHaveBeenCalledWith(
123456789,
20,
0
);
expect(getMessagesApi).toHaveBeenCalledWith(123456789, 20, 0);
});
it("should handle messages without sender ID", async () => {
@@ -205,10 +206,7 @@ describe("getMessages tool handler", () => {
fromCache: false,
});
const result = await getMessages(
{ chat_id: "123456789" },
mockContext
);
const result = await getMessages({ chat_id: "123456789" }, mockContext);
expect(result.isError).toBeFalsy();
const text = result.content[0].text;
@@ -224,14 +222,9 @@ describe("getMessages tool handler", () => {
};
vi.mocked(getChatById).mockReturnValue(mockChat as any);
vi.mocked(getMessagesApi).mockRejectedValue(
new Error("Network error")
);
vi.mocked(getMessagesApi).mockRejectedValue(new Error("Network error"));
const result = await getMessages(
{ chat_id: "123456789" },
mockContext
);
const result = await getMessages({ chat_id: "123456789" }, mockContext);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("MSG-ERR");
@@ -1,7 +1,7 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { sendMessage, tool } from "../sendMessage";
import type { TelegramMCPContext } from "../../types";
import type { TelegramState } from "../../../../../store/telegram/types";
import type { TelegramState } from "../../../../store/telegram/types";
vi.mock("../../api/sendMessage");
@@ -26,20 +26,14 @@ describe("sendMessage tool handler", () => {
});
it("should return error when chat_id is missing", async () => {
const result = await sendMessage(
{ message: "test message" },
mockContext
);
const result = await sendMessage({ message: "test message" }, mockContext);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("chat_id");
});
it("should return error when message is missing", async () => {
const result = await sendMessage(
{ chat_id: "123456789" },
mockContext
);
const result = await sendMessage({ chat_id: "123456789" }, mockContext);
expect(result.isError).toBe(true);
expect(result.content[0].text).toBe("Message content is required");
@@ -82,10 +76,7 @@ describe("sendMessage tool handler", () => {
expect(result.isError).toBeFalsy();
expect(result.content[0].text).toBe("Message sent successfully.");
// validateId converts string to number
expect(sendMessageApi).toHaveBeenCalledWith(
123456789,
"Hello, world!"
);
expect(sendMessageApi).toHaveBeenCalledWith(123456789, "Hello, world!");
});
it("should return error when API returns undefined", async () => {
@@ -107,9 +98,7 @@ describe("sendMessage tool handler", () => {
});
it("should handle API errors gracefully", async () => {
vi.mocked(sendMessageApi).mockRejectedValue(
new Error("Network error")
);
vi.mocked(sendMessageApi).mockRejectedValue(new Error("Network error"));
const result = await sendMessage(
{
@@ -1,7 +1,7 @@
import { describe, it, expect } from "vitest";
import * as tools from "../index";
import { TELEGRAM_MCP_TOOL_NAMES } from "../../types";
import type { MCPTool } from "../../../types";
import type { MCPTool } from "../../../mcp/types";
describe("Tool Definitions", () => {
// Extract tool definitions (exports ending with "Tool")
@@ -96,7 +96,14 @@ describe("Tool Definitions", () => {
});
it("should have valid property types in inputSchema", () => {
const validTypes = ["string", "number", "integer", "boolean", "array", "object"];
const validTypes = [
"string",
"number",
"integer",
"boolean",
"array",
"object",
];
const toolDefs = getToolDefinitions();
toolDefs.forEach(({ tool }) => {
@@ -112,14 +119,16 @@ describe("Tool Definitions", () => {
const toolDefs = getToolDefinitions();
toolDefs.forEach(({ tool }) => {
Object.entries(tool.inputSchema.properties).forEach(([propName, prop]: [string, any]) => {
expect(
prop.description,
`Property "${propName}" in tool "${tool.name}" should have a description`
).toBeDefined();
expect(typeof prop.description).toBe("string");
expect(prop.description.length).toBeGreaterThan(0);
});
Object.entries(tool.inputSchema.properties).forEach(
([propName, prop]: [string, any]) => {
expect(
prop.description,
`Property "${propName}" in tool "${tool.name}" should have a description`
).toBeDefined();
expect(typeof prop.description).toBe("string");
expect(prop.description.length).toBeGreaterThan(0);
}
);
});
});
@@ -1,6 +1,6 @@
import type { MCPTool, MCPToolResult } from "../../types";
import type { MCPTool, MCPToolResult } from "../../mcp/types";
import type { TelegramMCPContext } from "../types";
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
import { ErrorCategory, logAndFormatError } from "../../mcp/errorHandler";
import { optString } from "../args";
import { addContact as addContactApi } from "../api/addContact";
@@ -20,7 +20,7 @@ export const tool: MCPTool = {
export async function addContact(
args: Record<string, unknown>,
_context: TelegramMCPContext,
_context: TelegramMCPContext
): Promise<MCPToolResult> {
try {
const phone = typeof args.phone === "string" ? args.phone : "";
@@ -42,7 +42,7 @@ export async function addContact(
return logAndFormatError(
"add_contact",
error instanceof Error ? error : new Error(String(error)),
ErrorCategory.CONTACT,
ErrorCategory.CONTACT
);
}
}
@@ -1,7 +1,7 @@
import type { MCPTool, MCPToolResult } from "../../types";
import type { MCPTool, MCPToolResult } from "../../mcp/types";
import type { TelegramMCPContext } from "../types";
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
import { validateId } from "../../validation";
import { ErrorCategory, logAndFormatError } from "../../mcp/errorHandler";
import { validateId } from "../../mcp/validation";
import { archiveChat as archiveChatApi } from "../api/archiveChat";
export const tool: MCPTool = {
@@ -18,7 +18,7 @@ export const tool: MCPTool = {
export async function archiveChat(
args: Record<string, unknown>,
_context: TelegramMCPContext,
_context: TelegramMCPContext
): Promise<MCPToolResult> {
try {
const chatId = validateId(args.chat_id, "chat_id");
@@ -31,7 +31,7 @@ export async function archiveChat(
return logAndFormatError(
"archive_chat",
error instanceof Error ? error : new Error(String(error)),
ErrorCategory.CHAT,
ErrorCategory.CHAT
);
}
}
@@ -1,7 +1,7 @@
import type { MCPTool, MCPToolResult } from "../../types";
import type { MCPTool, MCPToolResult } from "../../mcp/types";
import type { TelegramMCPContext } from "../types";
import { ErrorCategory, logAndFormatError } from "../../errorHandler";
import { validateId } from "../../validation";
import { ErrorCategory, logAndFormatError } from "../../mcp/errorHandler";
import { validateId } from "../../mcp/validation";
import { banUser as banUserApi } from "../api/banUser";
import { getChatById } from "../api/helpers";
@@ -20,7 +20,7 @@ export const tool: MCPTool = {
export async function banUser(
args: Record<string, unknown>,
_context: TelegramMCPContext,
_context: TelegramMCPContext
): Promise<MCPToolResult> {
try {
const chatId = validateId(args.chat_id, "chat_id");
@@ -41,7 +41,7 @@ export async function banUser(
return logAndFormatError(
"ban_user",
error instanceof Error ? error : new Error(String(error)),
ErrorCategory.ADMIN,
ErrorCategory.ADMIN
);
}
}

Some files were not shown because too many files have changed in this diff Show More