Skip to content

feat: add tgcli folders commands for Chat Folders (CRUD + shared folders) #3

Description

@dapi

Summary

Add full CRUD support for Telegram Chat Folders (Dialog Filters) via tgcli folders commands. Includes support for shared/chatlist folders.

Proposed Commands

tgcli folders list                          # List all folders
tgcli folders show <id|name>                # Folder details (chats, filters)
tgcli folders create --title <name>         # Create folder
tgcli folders edit <id|name>                # Edit folder
tgcli folders delete <id|name>              # Delete folder
tgcli folders reorder --ids <id1,id2,...>   # Reorder folders
tgcli folders chats add <id|name> --chat <chatId>    # Add chat to folder
tgcli folders chats remove <id|name> --chat <chatId> # Remove chat from folder
tgcli folders join <invite-link>            # Join shared folder

Create/Edit Options

Flag Description
--title Folder name (max 12 chars)
--emoji Emoji icon
--include-contacts Include contacts
--include-non-contacts Include non-contacts
--include-groups Include groups
--include-channels Include channels
--include-bots Include bots
--exclude-muted Exclude muted
--exclude-read Exclude read
--exclude-archived Exclude archived
--chat Chat to include (repeatable)
--exclude-chat Chat to exclude (repeatable)
--pin-chat Pin chat in folder

Technical Notes

  • All required methods already available in @mtcute/core: getFolders, createFolder, editFolder, deleteFolder, findFolder, setFoldersOrder, joinChatlist
  • Pattern follows existing channels/groups/contacts commands
  • Folder resolution by ID or title (like existing chat resolution patterns)
  • show command needs to resolve included peers to display chat names
  • Listing chats in a folder is slow (Telegram API limitation: must fetch all dialogs and filter)

Implementation Plan

Task 1: Add folder domain methods to telegram-client.js

Files: Modify telegram-client.js (add methods after existing listForumTopics at ~line 1196)

Methods to add:

  • getFolders() — list all folders with normalized output
  • findFolder(idOrName) — resolve folder by numeric ID or title string
  • showFolder(idOrName) — detailed folder info with filter flags and peer lists
  • createFolder(options) — create folder with title, emoji, filter flags, peer lists
  • editFolder(idOrName, modification) — modify existing folder properties
  • deleteFolder(idOrName) — delete folder by ID or title
  • setFoldersOrder(ids) — reorder folders
  • addChatToFolder(idOrName, chatId) — append chat to folder's includePeers
  • removeChatFromFolder(idOrName, chatId) — remove chat from folder's includePeers
  • joinChatlist(link) — join shared folder via invite link
async getFolders() {
  await this.ensureLogin();
  const result = await this.client.getFolders();
  return result.filters.map((f) => {
    if (f._ === 'dialogFilterDefault') return { id: 0, title: 'All Chats', type: 'default' };
    return {
      id: f.id,
      title: typeof f.title === 'string' ? f.title : f.title.text,
      emoji: f.emoticon ?? null,
      color: f.color ?? null,
      type: f._ === 'dialogFilterChatlist' ? 'chatlist' : 'filter',
      contacts: f.contacts ?? false,
      nonContacts: f.nonContacts ?? false,
      groups: f.groups ?? false,
      broadcasts: f.broadcasts ?? false,
      bots: f.bots ?? false,
      excludeMuted: f.excludeMuted ?? false,
      excludeRead: f.excludeRead ?? false,
      excludeArchived: f.excludeArchived ?? false,
      includePeers: f.includePeers?.length ?? 0,
      excludePeers: f.excludePeers?.length ?? 0,
      pinnedPeers: f.pinnedPeers?.length ?? 0,
    };
  });
}

async findFolder(idOrName) {
  await this.ensureLogin();
  const id = Number(idOrName);
  if (!isNaN(id)) return this.client.findFolder({ id });
  return this.client.findFolder({ title: String(idOrName) });
}

async showFolder(idOrName) {
  await this.ensureLogin();
  const folder = await this.findFolder(idOrName);
  if (!folder) throw new Error(`Folder not found: ${idOrName}`);
  return {
    id: folder.id,
    title: typeof folder.title === 'string' ? folder.title : folder.title.text,
    emoji: folder.emoticon ?? null, color: folder.color ?? null,
    type: folder._ === 'dialogFilterChatlist' ? 'chatlist' : 'filter',
    contacts: folder.contacts ?? false, nonContacts: folder.nonContacts ?? false,
    groups: folder.groups ?? false, broadcasts: folder.broadcasts ?? false, bots: folder.bots ?? false,
    excludeMuted: folder.excludeMuted ?? false, excludeRead: folder.excludeRead ?? false, excludeArchived: folder.excludeArchived ?? false,
    includePeers: folder.includePeers ?? [], excludePeers: folder.excludePeers ?? [], pinnedPeers: folder.pinnedPeers ?? [],
  };
}

async createFolder(options) {
  await this.ensureLogin();
  const params = { title: options.title };
  if (options.emoji) params.emoticon = options.emoji;
  if (options.contacts) params.contacts = true;
  if (options.nonContacts) params.nonContacts = true;
  if (options.groups) params.groups = true;
  if (options.broadcasts) params.broadcasts = true;
  if (options.bots) params.bots = true;
  if (options.excludeMuted) params.excludeMuted = true;
  if (options.excludeRead) params.excludeRead = true;
  if (options.excludeArchived) params.excludeArchived = true;
  if (options.includePeers?.length) params.includePeers = options.includePeers;
  if (options.excludePeers?.length) params.excludePeers = options.excludePeers;
  if (options.pinnedPeers?.length) params.pinnedPeers = options.pinnedPeers;
  const result = await this.client.createFolder(params);
  return { id: result.id, title: typeof result.title === 'string' ? result.title : result.title.text };
}

async editFolder(idOrName, modification) {
  await this.ensureLogin();
  const folder = await this.findFolder(idOrName);
  if (!folder) throw new Error(`Folder not found: ${idOrName}`);
  const mod = {};
  if (modification.title !== undefined) mod.title = modification.title;
  if (modification.emoji !== undefined) mod.emoticon = modification.emoji;
  if (modification.contacts !== undefined) mod.contacts = modification.contacts;
  if (modification.nonContacts !== undefined) mod.nonContacts = modification.nonContacts;
  if (modification.groups !== undefined) mod.groups = modification.groups;
  if (modification.broadcasts !== undefined) mod.broadcasts = modification.broadcasts;
  if (modification.bots !== undefined) mod.bots = modification.bots;
  if (modification.excludeMuted !== undefined) mod.excludeMuted = modification.excludeMuted;
  if (modification.excludeRead !== undefined) mod.excludeRead = modification.excludeRead;
  if (modification.excludeArchived !== undefined) mod.excludeArchived = modification.excludeArchived;
  if (modification.includePeers !== undefined) mod.includePeers = modification.includePeers;
  if (modification.excludePeers !== undefined) mod.excludePeers = modification.excludePeers;
  if (modification.pinnedPeers !== undefined) mod.pinnedPeers = modification.pinnedPeers;
  const result = await this.client.editFolder({ folder, modification: mod });
  return { id: result.id, title: typeof result.title === 'string' ? result.title : result.title.text };
}

async deleteFolder(idOrName) {
  await this.ensureLogin();
  const folder = await this.findFolder(idOrName);
  if (!folder) throw new Error(`Folder not found: ${idOrName}`);
  await this.client.deleteFolder(folder.id);
  return { deleted: true, id: folder.id };
}

async setFoldersOrder(ids) {
  await this.ensureLogin();
  await this.client.setFoldersOrder(ids.map(Number));
  return { ok: true };
}

async addChatToFolder(idOrName, chatId) {
  await this.ensureLogin();
  const folder = await this.findFolder(idOrName);
  if (!folder) throw new Error(`Folder not found: ${idOrName}`);
  const peers = folder.includePeers ? [...folder.includePeers] : [];
  peers.push(chatId);
  await this.client.editFolder({ folder, modification: { includePeers: peers } });
  return { ok: true, folderId: folder.id };
}

async removeChatFromFolder(idOrName, chatId) {
  await this.ensureLogin();
  const folder = await this.findFolder(idOrName);
  if (!folder) throw new Error(`Folder not found: ${idOrName}`);
  const chatIdStr = String(chatId);
  const peers = (folder.includePeers ?? []).filter((p) => {
    const peerId = typeof p === 'object' && p !== null ? String(p.userId ?? p.channelId ?? p.chatId ?? '') : String(p);
    return peerId !== chatIdStr;
  });
  await this.client.editFolder({ folder, modification: { includePeers: peers } });
  return { ok: true, folderId: folder.id };
}

async joinChatlist(link) {
  await this.ensureLogin();
  const result = await this.client.joinChatlist(link);
  return { id: result.id, title: typeof result.title === 'string' ? result.title : result.title.text, type: 'chatlist' };
}

Commit: feat(folders): add folder domain methods to telegram-client


Task 2: Add CLI subcommands in cli.js

Files: Modify cli.js (insert folders block before disableHelpCommand(program) at ~line 408; add handler functions)

Commander subcommands: folders list, folders show <folder>, folders create, folders edit <folder>, folders delete <folder>, folders reorder, folders join <link>, folders chats add <folder>, folders chats remove <folder>.

Handler functions follow same pattern as runGroupsList/runGroupsInfo: resolveStoreDiracquireReadLockcreateServices → auth check → call domain method → writeJson/console output → cleanup in finally.

Commit: feat(folders): add CLI subcommands for folder management


Task 3: Add MCP tools in mcp-server.js

Files: Modify mcp-server.js (add Zod schemas near top ~line 150, register tools near end ~line 1270)

9 MCP tools: listFolders, showFolder, createFolder, editFolder, deleteFolder, reorderFolders, addChatToFolder, removeChatFromFolder, joinChatlist.

Pattern follows existing server.tool(name, description, schema, handler) with await telegramClient.ensureLogin() and JSON response.

Commit: feat(folders): add MCP tools for folder management


Task 4: Smoke test all commands

node cli.js folders list --json --timeout 30s
node cli.js folders create --title "Test" --emoji "🧪" --json --timeout 30s
node cli.js folders show <id> --json --timeout 30s
node cli.js folders delete <id> --json --timeout 30s
node cli.js folders --help

Task 5: Update tgcli skill documentation

Add ### Folders section to skill file:

tgcli folders list --json --timeout 30s
tgcli folders show <id|name> --json --timeout 30s
tgcli folders create --title "AI channels" --emoji "🤖" --include-channels --json --timeout 30s
tgcli folders edit <id|name> --title "New name" --json --timeout 30s
tgcli folders delete <id|name> --json --timeout 30s
tgcli folders reorder --ids 1,2,3 --json --timeout 30s
tgcli folders chats add <id|name> --chat <chatId> --json --timeout 30s
tgcli folders chats remove <id|name> --chat <chatId> --json --timeout 30s
tgcli folders join <invite-link> --json --timeout 30s

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions