Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,12 @@ function buildProgram() {
.option('--enable', 'Enable sync')
.option('--disable', 'Disable sync')
.action(withGlobalOptions((globalFlags, options) => runChannelsSync(globalFlags, options)));
channels
.command('mark-read')
.description('Mark channel as read up to a message')
.option('--chat <id|username>', 'Channel identifier')
.option('--message-id <n>', 'Mark as read up to this message ID')
.action(withGlobalOptions((globalFlags, options) => runChannelsMarkRead(globalFlags, options)));

const messages = program.command('messages').description('List and search messages');
messages
Expand All @@ -178,6 +184,7 @@ function buildProgram() {
.option('--after <iso>', 'Filter messages after date')
.option('--before <iso>', 'Filter messages before date')
.option('--limit <n>', 'Limit results')
.option('--offset-id <id>', 'Fetch messages older than this message ID (for pagination)')
.action(withGlobalOptions((globalFlags, options) => runMessagesList(globalFlags, options)));
messages
.command('search')
Expand Down Expand Up @@ -2058,6 +2065,40 @@ async function runChannelsSync(globalFlags, options = {}) {
}, timeoutMs);
}

async function runChannelsMarkRead(globalFlags, options = {}) {
const timeoutMs = globalFlags.timeoutMs;
return runWithTimeout(async () => {
if (!options.chat) {
throw new Error('--chat is required');
}
if (!options.messageId) {
throw new Error('--message-id is required');
}
const messageId = parsePositiveInt(options.messageId, '--message-id');
if (messageId === null) {
throw new Error('--message-id must be a positive integer');
}
const storeDir = resolveStoreDir();
const release = acquireStoreLock(storeDir);
const { telegramClient, messageSyncService } = createServices({ storeDir });
try {
if (!(await telegramClient.isAuthorized().catch(() => false))) {
throw new Error('Not authenticated. Run `tgcli auth` first.');
}
const result = await telegramClient.markChannelRead(options.chat, messageId);
if (globalFlags.json) {
writeJson(result);
} else {
console.log(`Marked channel ${result.channelId} as read up to message ${result.messageId}.`);
}
} finally {
await messageSyncService.shutdown();
await telegramClient.destroy();
release();
}
}, timeoutMs);
}

function createLiveMetadataResolver(messageSyncService, telegramClient) {
return async (channelId, fallback = {}) => {
const meta = messageSyncService.getChannelMetadata(channelId);
Expand Down Expand Up @@ -2111,7 +2152,12 @@ async function runMessagesList(globalFlags, options = {}) {
const response = await telegramClient.getTopicMessages(id, topicId, finalLimit);
liveMessages = response.messages;
} else {
const response = await telegramClient.getMessagesByChannelId(id, finalLimit);
const fetchOptions = {};
const offsetId = parsePositiveInt(options.offsetId, '--offset-id');
if (offsetId) {
fetchOptions.offsetId = offsetId;
}
const response = await telegramClient.getMessagesByChannelId(id, finalLimit, fetchOptions);
liveMessages = response.messages;
peerTitle = response.peerTitle ?? null;
}
Expand Down
30 changes: 29 additions & 1 deletion mcp-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,15 @@ const channelIdSchema = z.union([
z.string({ invalid_type_error: "channelId must be a string" }).min(1),
]);

const markChannelReadSchema = {
channelId: channelIdSchema.describe("Numeric channel ID or username"),
messageId: z
.number({ invalid_type_error: "messageId must be a number" })
.int()
.positive()
.describe("Mark as read up to this message ID (inclusive)"),
};

const userIdSchema = z.union([
z.number({ invalid_type_error: "userId must be a number" }),
z.string({ invalid_type_error: "userId must be a string" }).min(1),
Expand Down Expand Up @@ -623,7 +632,7 @@ function createServerInstance() {

server.tool(
"listChannels",
"Lists available Telegram dialogs for the authenticated account.",
"Lists available Telegram dialogs for the authenticated account, including unread message counts.",
listChannelsSchema,
async ({ limit }) => {
await telegramClient.ensureLogin();
Expand Down Expand Up @@ -1694,6 +1703,25 @@ function createServerInstance() {
},
);

server.tool(
"markChannelRead",
"Marks a Telegram channel as read up to the specified message ID.",
markChannelReadSchema,
async ({ channelId, messageId }) => {
await telegramClient.ensureLogin();
const result = await telegramClient.markChannelRead(channelId, messageId);

return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
},
);

return server;
}

Expand Down
22 changes: 22 additions & 0 deletions telegram-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,8 @@ class TelegramClient {
chatType,
isForum,
isGroup,
unreadCount: typeof dialog.unreadCount === 'number' ? dialog.unreadCount : 0,
unreadMentionsCount: typeof dialog.unreadMentionsCount === 'number' ? dialog.unreadMentionsCount : 0,
});

if (results.length >= effectiveLimit) {
Expand Down Expand Up @@ -621,6 +623,8 @@ class TelegramClient {
chatType,
isForum,
isGroup,
unreadCount: typeof dialog.unreadCount === 'number' ? dialog.unreadCount : 0,
unreadMentionsCount: typeof dialog.unreadMentionsCount === 'number' ? dialog.unreadMentionsCount : 0,
});
}

Expand All @@ -639,6 +643,7 @@ class TelegramClient {
minId = 0,
maxId = 0,
reverse = false,
offsetId = 0,
} = options;
const peerRef = normalizeChannelId(channelId);
const peer = await this.client.resolvePeer(peerRef);
Expand All @@ -660,6 +665,11 @@ class TelegramClient {
iterOptions.maxId = maxId;
}

if (offsetId) {
iterOptions.offset = { id: offsetId, date: 0 };
iterOptions.addOffset = 0;
}

for await (const message of this.client.iterHistory(peer, iterOptions)) {
messages.push(this._serializeMessage(message, peer));
if (messages.length >= effectiveLimit) {
Expand Down Expand Up @@ -973,6 +983,18 @@ class TelegramClient {
return true;
}

async markChannelRead(channelId, messageId) {
await this.ensureLogin();
const msgId = Number(messageId);
if (!Number.isInteger(msgId) || msgId <= 0) {
throw new Error('messageId must be a positive integer');
}
const peerRef = normalizeChannelId(channelId);
const peer = await this.client.resolvePeer(peerRef);
await this.client.readHistory(peer, { maxId: msgId });
return { channelId: peer?.id?.toString?.() ?? String(channelId), messageId: msgId };
}

async getPeerMetadata(channelId, peerType) {
await this.ensureLogin();
const peerRef = normalizeChannelId(channelId);
Expand Down