
Telegram
- 435 installs
- 11 repo stars
- Updated June 1, 2026
- skillhq/telegram
telegram is a developer skill that helps build Telegram bots and webhooks to send messages, handle commands, inline keyboards, and channel notifications from a backend or agent service.
About
telegram is a skillhq skill that teaches coding agents to integrate Telegram Bot API features into backend and agent services. It covers webhook setup, command routing, inline keyboard interactions, and channel notification patterns so services can push alerts and accept user input through Telegram. Developers reach for telegram when adding bot commands to ops dashboards, wiring agent replies to a messaging channel, or broadcasting deployment and monitoring events to a team channel. The skill assumes standard Telegram bot architecture—HTTP webhooks or polling, token-authenticated requests, and handler maps for updates—rather than generic chat UI work. Use it while scaffolding notification pipelines, conversational agent interfaces, or lightweight mobile-friendly control surfaces without building a separate client app.
- Bot token setup and webhook configuration
- Command handlers and inline keyboard UX
- Outbound alerts and scheduled messaging
- Secure handling of chat IDs and API secrets
Telegram by the numbers
- 435 all-time installs (skills.sh)
- +8 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,008 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/skillhq/telegram --skill telegramAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 435 |
|---|---|
| repo stars | ★ 11 |
| Last updated | June 1, 2026 |
| Repository | skillhq/telegram ↗ |
How do you build Telegram bot webhooks and commands?
Build Telegram bots and webhooks—send messages, handle commands, inline keyboards, and channel notifications from your backend or agent.
Who is it for?
Backend developers adding Telegram bots, alerts, or command interfaces to APIs, agents, or internal tooling.
Skip if: Projects needing Slack, Discord, or SMS integrations without Telegram, or teams prohibited from third-party messaging APIs.
When should I use this skill?
A developer asks to send Telegram messages, register bot commands, configure webhooks, or add inline keyboard interactions from a service.
What you get
Telegram bot handlers, webhook endpoints, inline keyboard flows, and channel notification integrations.
- webhook handlers
- command routers
- notification integrations
Files
📬 Telegram CLI
Fast Telegram CLI for reading, searching, and sending messages.
🎯 When to Use
Use this skill when the user:
- Asks to check Telegram messages or inbox
- Wants to search Telegram for a topic/keyword
- Wants to send a Telegram message or reply to one
- Asks about a Telegram group, contact, or chat
- Wants to see unread messages
- Needs to look up group members or admins
- Wants to mute/unmute a noisy chat or group
- Needs to kick/remove a user from a group
- Wants to promote a member to admin
- Wants to transfer ownership of a group/channel to someone else
- Wants to export or sync chat history to files
- Asks to organize chats into folders
- Wants to check their logged-in account or session status
📦 Install
npm install -g @skillhq/telegram🔐 Authentication
First-time setup requires API credentials from https://my.telegram.org/apps
telegram auth # First-time login
telegram logout # Clear saved session
telegram check # Verify session is valid
telegram whoami # Show logged-in account
telegram whoami --json # Account info as JSON📖 Commands
Reading Messages
telegram inbox # Unread messages summary
telegram chats # List all chats
telegram chats --type group # Filter: user, group, supergroup, channel
telegram chats -n 200 # List up to 200 chats
telegram read "ChatName" -n 50 # Read last 50 messages
telegram read "ChatName" --since "1h" # Messages from last hour
telegram read "ChatName" --until "2h" # Messages up to 2 hours ago
telegram read @username -n 20 # Read DM with user
telegram read 123456789 -n 10 # Read by chat IDSearching
telegram search "query" --chat "ChatName" # Search within chat
telegram search "query" --all # Search all chats (global)
telegram search "query" -n 20 # Limit resultsSending Messages
telegram send @username "message" # Send DM
telegram send "GroupName" "message" # Send to group
telegram reply "ChatName" 12345 "response" # Reply to message IDContacts & Groups
telegram contact @username # Get contact info
telegram members "GroupName" # List group members
telegram members "GroupName" -n 500 # Fetch up to 500 members
telegram admins "GroupName" # List admins only
telegram groups # List all groups
telegram groups --admin # Groups where you're admin
telegram kick "GroupName" @username # Remove user from group
telegram promote "GroupName" @username # Promote a member to admin
telegram promote "GroupName" @username --rank "Mod" # Promote with a custom admin title
telegram promote "GroupName" @username --add-admins # Allow them to add admins too
telegram transfer-owner "GroupName" @username # Hand over ownership (prompts for 2FA password)`transfer-owner` notes:
- Irreversible: you drop to a regular admin/member and only the new owner can transfer it back.
- Requires two-step verification (cloud password) on your account; it is prompted for securely at runtime.
- Supergroups and channels only - convert a basic group to a supergroup first.
- The target must already be a member. Telegram also blocks transfer for ~24h after a new login and ~7 days after setting/changing your 2FA password.
- Prompts you to retype the group name to confirm; pass
-yto skip that confirmation.
Muting
telegram mute "ChatName" # Mute forever
telegram mute "ChatName" -d 1h # Mute for 1 hour
telegram mute @username -d 8h # Mute DM for 8 hours
telegram mute "GroupName" -d 1d # Mute for 1 day
telegram unmute "ChatName" # UnmuteFolders
telegram folders # List all folders
telegram folder "Work" # Show chats in folder
telegram folder-add "Work" "ProjectChat" # Add chat to folder
telegram folder-remove "Work" "ProjectChat" # Remove chat from folderSync / Export
telegram sync # Sync last 7 days to ./telegram-sync
telegram sync --days 30 # Sync last 30 days
telegram sync --since "12h" # Sync messages from last 12 hours
telegram sync --until "2d" # Sync messages up to 2 days ago
telegram sync --all # Sync entire chat history (no time limit)
telegram sync --chat "ChatName" # Sync specific chat only
telegram sync --output ~/exports # Custom output directory
telegram sync --resume # Incremental: only fetch new messages
telegram sync --resume --all # Keep a complete archive up to dateIncremental sync (`--resume`):
- Tracks last synced message ID per chat in
.sync-meta.json - On subsequent runs, only fetches messages newer than last sync
- Appends new messages to existing markdown files
- Combine with
--allto maintain a complete, up-to-date archive
📤 Output Formats
Most commands support multiple output formats:
| Flag | Use Case |
|---|---|
| (default) | Human-readable terminal output |
--json | Structured JSON for programmatic processing |
--markdown | Markdown-formatted for display or export |
telegram inbox --json # JSON format
telegram inbox --markdown # Markdown format
telegram read "Chat" --json # JSON with messages array
telegram read "Chat" --markdown # Markdown with messages
telegram chats --json # JSON with chat list
telegram members "Group" --markdown # Markdown member listSupported on: inbox, read, search, chats, members, groups, contact, whoami
📎 Media Metadata
Messages containing media (photos, videos, documents, voice notes, stickers, etc.) now include metadata instead of showing "(no text)":
| Media Type | Display |
|---|---|
| Photo | [📷 Photo] |
| Video | [🎥 Video (2.1 MB)] |
| Document | [📎 report.pdf (540.0 KB)] |
| Voice | [🎤 Voice message] |
| Audio | [🎵 song.mp3 (3.2 MB)] |
| Sticker | [😀 Sticker] |
| GIF | [🎬 GIF] |
| Location | [📍 Location] |
| Contact | [👤 Contact] |
| Poll | [📊 Poll] |
In JSON output, messages include mediaType, fileName, and fileSize fields when media is present.
🤖 AI Agent Guidance
When using this CLI as an AI agent:
- For processing data (counting, filtering, extracting): use
--json - For displaying to the user: use default or
--markdown - Chat identification: names are partial-matched (e.g., "MetaDAO" matches "MetaDAO Community"), usernames must start with
@, numeric IDs also work - Read operations are safe to run without confirmation
- Write operations (
send,reply,kick,promote,transfer-owner) should be confirmed with the user before executing - `transfer-owner` is irreversible and interactive (it prompts for a 2FA password and a typed confirmation), so it cannot be run unattended; never script it on a user's behalf without explicit instruction
- Rate limiting: avoid rapid successive calls; the Telegram API has rate limits
- Large groups: use
-nto limitmembersoutput on very large groups - Full archive: use
telegram sync --all --chat "Name"to export complete chat history - Keeping archives fresh: use
telegram sync --resumeto incrementally update previous exports - Media-rich chats: messages with photos/videos/files now show metadata, not just "(no text)"
💡 Examples
Check inbox for unread messages:
telegram inboxRead recent messages from a group:
telegram read "MetaDAO Community" -n 20Get messages from the last 2 hours:
telegram read "Project Chat" --since "2h"Search for a topic across all chats:
telegram search "futarchy" --allSearch within a specific chat:
telegram search "deadline" --chat "Work Team"Send a message:
telegram send @username "Hello, checking in!"Export a chat's complete history:
telegram sync --all --chat "Project Chat" --output ~/exportsIncrementally update an existing export:
telegram sync --resume --output ~/exportsFilter chats by type:
telegram chats --type channel --jsonKick a user from a group:
telegram kick "My Group" @spammerPromote a member to admin:
telegram promote "My Group" @trustedmemberTransfer ownership of a group to someone else:
telegram transfer-owner "My Group" @newowner📝 Notes
- Chat names can be partial matches (e.g., "MetaDAO" matches "MetaDAO Community")
- Usernames must start with
@(e.g.,@username) - Chat IDs (numeric) can be used anywhere a chat name is accepted
- Messages are returned in reverse chronological order (newest first)
- Time flags (
--since,--until) accept formats like"1h","30m","7d" - The
synccommand creates one markdown file per chat in the output directory - Sync metadata (
.sync-meta.json) enables incremental sync with--resume - Sender names are cached per request for faster syncs (avoids redundant API calls)
- Messages paginate automatically — no silent truncation for large chats
# Only publish SKILL.md and README.md for the skill
# Exclude everything else
# Source code
src/
dist/
scripts/
# Config files
*.json
*.json5
*.yaml
*.yml
*.toml
tsconfig*.json
package-lock.json
# Docs (except README and SKILL)
CLAUDE.md
CONTRIBUTING.md
LICENSE
# GitHub
.github/
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run build
name: Publish to npm
on:
push:
tags:
- 'v*'
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
registry-url: https://registry.npmjs.org
- run: npm ci
- run: npm run build
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
node_modules/
dist/
*.log
.DS_Store
# Credentials and config files (contain API keys and session tokens)
.tgrc.json5
config.json5
.env
.env.*
Claude Instructions for @skillhq/telegram
Version and Release Workflow
When using /commit-and-push or committing changes:
1. Always bump the version in package.json before committing:
- Patch (0.x.Y): Bug fixes
- Minor (0.X.0): New features (like new commands)
- Major (X.0.0): Breaking changes
2. After pushing, always create and push a git tag:
git tag v<version>
git push origin v<version>3. Tag naming: Use v prefix (e.g., v0.3.0, v1.0.0)
Commit Message Format
Follow conventional commits style:
Add <feature>for new featuresFix <bug>for bug fixesUpdate <component>for changes- Include
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>at the end
#!/bin/sh
# Telegram CLI installer & onboarding
# Usage: curl -sSfL https://github.com/skillhq/telegram/raw/main/install.sh | bash
#
# Installs @skillhq/telegram globally, offers to install companion skills,
# and optionally runs a test query to verify setup.
set -e
PACKAGE="@skillhq/telegram"
BINARY="telegram"
main() {
need_cmd node
need_cmd npm
# --- Install CLI ---
if has "$BINARY"; then
current_version=$("$BINARY" --version 2>/dev/null || echo "unknown")
log "Telegram CLI already installed (v${current_version})."
printf " Reinstall / upgrade? [y/N] " >&2
if has_tty; then
read -r answer < /dev/tty || answer=""
else
answer=""
fi
case "$answer" in
[yY]*)
log "Upgrading ${PACKAGE}..."
npm install -g --no-fund --no-audit "$PACKAGE" || err "npm install failed. Try: sudo npm install -g $PACKAGE"
;;
*)
log "Skipping install."
;;
esac
else
log "Installing ${PACKAGE}..."
npm install -g --no-fund --no-audit "$PACKAGE" || err "npm install failed. Try: sudo npm install -g $PACKAGE"
fi
# Post-install interactive setup only when we have a usable terminal.
if has_tty; then
post_install
else
echo "" >&2
log "Telegram CLI installed successfully!"
echo "" >&2
log "Tip: Run 'npx skills add https://github.com/skillhq/telegram' to install the Telegram skill."
log "Tip: Run 'telegram auth' to authenticate with your Telegram account."
fi
}
post_install() {
echo "" >&2
# --- Skill install ---
if has npx; then
if npx --yes skills list 2>/dev/null | grep -q "skillhq/telegram"; then
log "Telegram skill already installed."
else
log "The Telegram skill lets AI agents (Cursor, Claude Code, etc.) read,"
log "search, and send Telegram messages on your behalf."
printf " Install Telegram skill for your AI agent? [Y/n] " >&2
read -r answer < /dev/tty || answer=""
case "$answer" in
[nN]*)
log "Skipped. You can install it later with: npx skills add https://github.com/skillhq/telegram"
;;
*)
log "Installing Telegram skill..."
npx --yes skills add https://github.com/skillhq/telegram < /dev/tty || log "Skill installation failed. You can retry with: npx skills add https://github.com/skillhq/telegram"
;;
esac
fi
else
log "Tip: Run 'npx skills add https://github.com/skillhq/telegram' to install the Telegram skill."
log "(requires Node.js / npx)"
fi
echo "" >&2
# --- Authentication ---
if "$BINARY" check > /dev/null 2>&1; then
log "Already authenticated with Telegram."
else
log "Authenticate to start reading and sending messages."
printf " Authenticate with Telegram now? [Y/n] " >&2
read -r answer < /dev/tty || answer=""
case "$answer" in
[nN]*)
log "Skipped. You can authenticate later with: telegram auth"
;;
*)
echo "" >&2
log "You'll need Telegram API credentials."
log "Go to https://my.telegram.org/apps to get your API ID and Hash."
echo "" >&2
"$BINARY" auth < /dev/tty || log "Authentication failed. You can retry with: telegram auth"
;;
esac
fi
echo "" >&2
# --- Test query ---
if "$BINARY" check > /dev/null 2>&1; then
printf " Run a test query to verify your setup? [Y/n] " >&2
read -r answer < /dev/tty || answer=""
case "$answer" in
[nN]*)
log "Skipped."
;;
*)
log "Fetching your account info..."
echo "" >&2
"$BINARY" whoami || log "Test query failed. Check your session with: telegram check"
echo "" >&2
log "Fetching unread inbox..."
echo "" >&2
"$BINARY" inbox -n 5 || log "Inbox query failed."
;;
esac
fi
echo "" >&2
log "Telegram CLI setup complete! 🎉"
echo "" >&2
log "Quick start:"
log " telegram inbox — Check unread messages"
log " telegram read \"Chat\" — Read messages from a chat"
log " telegram search \"q\" — Search across chats"
log " telegram --help — See all commands"
}
has_tty() {
[ -t 0 ] || [ -t 1 ] || [ -t 2 ]
}
has() {
command -v "$1" > /dev/null 2>&1
}
need_cmd() {
if ! has "$1"; then
err "required command not found: $1"
fi
}
log() {
echo " $*" >&2
}
err() {
log "error: $*"
exit 1
}
main "$@"
{
"name": "@skillhq/telegram",
"version": "0.3.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@skillhq/telegram",
"version": "0.3.0",
"license": "MIT",
"dependencies": {
"big-integer": "^1.6.52",
"chalk": "^5.3.0",
"commander": "^14.0.0",
"json5": "^2.2.3",
"ora": "^8.1.0",
"telegram": "^2.26.0"
},
"bin": {
"telegram": "dist/index.js"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.7.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@cryptography/aes": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@cryptography/aes/-/aes-0.1.1.tgz",
"integrity": "sha512-PcYz4FDGblO6tM2kSC+VzhhK62vml6k6/YAkiWtyPvrgJVfnDRoHGDtKn5UiaRRUrvUTTocBpvc2rRgTCqxjsg==",
"license": "GPL-3.0-or-later"
},
"node_modules/@types/node": {
"version": "22.19.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz",
"integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/async-mutex": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.3.2.tgz",
"integrity": "sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.1"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/big-integer": {
"version": "1.6.52",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
"integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
"license": "Unlicense",
"engines": {
"node": ">=0.6"
}
},
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/bufferutil": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz",
"integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/chalk": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/cli-cursor": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
"integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
"license": "MIT",
"dependencies": {
"restore-cursor": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-spinners": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
"integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/commander": {
"version": "14.0.2",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz",
"integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==",
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/d": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",
"integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
"license": "ISC",
"dependencies": {
"es5-ext": "^0.10.64",
"type": "^2.7.2"
},
"engines": {
"node": ">=0.12"
}
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/dom-serializer": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
"integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==",
"license": "MIT",
"dependencies": {
"domelementtype": "^2.0.1",
"domhandler": "^4.2.0",
"entities": "^2.0.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "BSD-2-Clause"
},
"node_modules/domhandler": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
"integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.2.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/domutils": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
"integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
"license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "^1.0.1",
"domelementtype": "^2.2.0",
"domhandler": "^4.2.0"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/emoji-regex": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"license": "MIT"
},
"node_modules/entities": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
"license": "BSD-2-Clause",
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/es5-ext": {
"version": "0.10.64",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
"integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
"hasInstallScript": true,
"license": "ISC",
"dependencies": {
"es6-iterator": "^2.0.3",
"es6-symbol": "^3.1.3",
"esniff": "^2.0.1",
"next-tick": "^1.1.0"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/es6-iterator": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
"integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
"license": "MIT",
"dependencies": {
"d": "1",
"es5-ext": "^0.10.35",
"es6-symbol": "^3.1.1"
}
},
"node_modules/es6-symbol": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
"integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
"license": "ISC",
"dependencies": {
"d": "^1.0.2",
"ext": "^1.7.0"
},
"engines": {
"node": ">=0.12"
}
},
"node_modules/esniff": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
"integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
"license": "ISC",
"dependencies": {
"d": "^1.0.1",
"es5-ext": "^0.10.62",
"event-emitter": "^0.3.5",
"type": "^2.7.2"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/event-emitter": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
"integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
"license": "MIT",
"dependencies": {
"d": "1",
"es5-ext": "~0.10.14"
}
},
"node_modules/ext": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
"integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
"license": "ISC",
"dependencies": {
"type": "^2.7.2"
}
},
"node_modules/get-east-asian-width": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
"integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/htmlparser2": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
"integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==",
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "MIT",
"dependencies": {
"domelementtype": "^2.0.1",
"domhandler": "^4.0.0",
"domutils": "^2.5.2",
"entities": "^2.0.0"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
"license": "MIT",
"engines": {
"node": ">=0.8.19"
}
},
"node_modules/ip-address": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/is-interactive": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
"integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
"license": "MIT"
},
"node_modules/is-unicode-supported": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"license": "MIT",
"bin": {
"json5": "lib/cli.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/log-symbols": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz",
"integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==",
"license": "MIT",
"dependencies": {
"chalk": "^5.3.0",
"is-unicode-supported": "^1.3.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-symbols/node_modules/is-unicode-supported": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
"integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mime": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/mimic-function": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
"integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/next-tick": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
"license": "ISC"
},
"node_modules/node-gyp-build": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"license": "MIT",
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/node-localstorage": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/node-localstorage/-/node-localstorage-2.2.1.tgz",
"integrity": "sha512-vv8fJuOUCCvSPjDjBLlMqYMHob4aGjkmrkaE42/mZr0VT+ZAU10jRF8oTnX9+pgU9/vYJ8P7YT3Vd6ajkmzSCw==",
"license": "MIT",
"dependencies": {
"write-file-atomic": "^1.1.4"
},
"engines": {
"node": ">=0.12"
}
},
"node_modules/onetime": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
"integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
"license": "MIT",
"dependencies": {
"mimic-function": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ora": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz",
"integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==",
"license": "MIT",
"dependencies": {
"chalk": "^5.3.0",
"cli-cursor": "^5.0.0",
"cli-spinners": "^2.9.2",
"is-interactive": "^2.0.0",
"is-unicode-supported": "^2.0.0",
"log-symbols": "^6.0.0",
"stdin-discarder": "^0.2.2",
"string-width": "^7.2.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
},
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
"license": "MIT"
},
"node_modules/real-cancellable-promise": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/real-cancellable-promise/-/real-cancellable-promise-1.2.3.tgz",
"integrity": "sha512-hBI5Gy/55VEeeMtImMgEirD7eq5UmqJf1J8dFZtbJZA/3rB0pYFZ7PayMGueb6v4UtUtpKpP+05L0VwyE1hI9Q==",
"license": "MIT"
},
"node_modules/restore-cursor": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
"integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
"license": "MIT",
"dependencies": {
"onetime": "^7.0.0",
"signal-exit": "^4.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/slide": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz",
"integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==",
"license": "ISC",
"engines": {
"node": "*"
}
},
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"license": "MIT",
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks": {
"version": "2.8.7",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
"license": "MIT",
"dependencies": {
"ip-address": "^10.0.1",
"smart-buffer": "^4.2.0"
},
"engines": {
"node": ">= 10.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/stdin-discarder": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
"integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/store2": {
"version": "2.14.4",
"resolved": "https://registry.npmjs.org/store2/-/store2-2.14.4.tgz",
"integrity": "sha512-srTItn1GOvyvOycgxjAnPA63FZNwy0PTyUBFMHRM+hVFltAeoh0LmNBz9SZqUS9mMqGk8rfyWyXn3GH5ReJ8Zw==",
"license": "MIT"
},
"node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strip-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/telegram": {
"version": "2.26.22",
"resolved": "https://registry.npmjs.org/telegram/-/telegram-2.26.22.tgz",
"integrity": "sha512-EIj7Yrjiu0Yosa3FZ/7EyPg9s6UiTi/zDQrFmR/2Mg7pIUU+XjAit1n1u9OU9h2oRnRM5M+67/fxzQluZpaJJg==",
"license": "MIT",
"dependencies": {
"@cryptography/aes": "^0.1.1",
"async-mutex": "^0.3.0",
"big-integer": "^1.6.48",
"buffer": "^6.0.3",
"htmlparser2": "^6.1.0",
"mime": "^3.0.0",
"node-localstorage": "^2.2.1",
"pako": "^2.0.3",
"path-browserify": "^1.0.1",
"real-cancellable-promise": "^1.1.1",
"socks": "^2.6.2",
"store2": "^2.13.0",
"ts-custom-error": "^3.2.0",
"websocket": "^1.0.34"
},
"optionalDependencies": {
"bufferutil": "^4.0.3",
"utf-8-validate": "^5.0.5"
}
},
"node_modules/ts-custom-error": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz",
"integrity": "sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type": {
"version": "2.7.3",
"resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
"integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==",
"license": "ISC"
},
"node_modules/typedarray-to-buffer": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
"license": "MIT",
"dependencies": {
"is-typedarray": "^1.0.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/utf-8-validate": {
"version": "5.0.10",
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
"integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/websocket": {
"version": "1.0.35",
"resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.35.tgz",
"integrity": "sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==",
"license": "Apache-2.0",
"dependencies": {
"bufferutil": "^4.0.1",
"debug": "^2.2.0",
"es5-ext": "^0.10.63",
"typedarray-to-buffer": "^3.1.5",
"utf-8-validate": "^5.0.2",
"yaeti": "^0.0.6"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/write-file-atomic": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz",
"integrity": "sha512-SdrHoC/yVBPpV0Xq/mUZQIpW2sWXAShb/V4pomcJXh92RuaO+f3UTWItiR3Px+pLnV2PvC2/bfn5cwr5X6Vfxw==",
"license": "ISC",
"dependencies": {
"graceful-fs": "^4.1.11",
"imurmurhash": "^0.1.4",
"slide": "^1.1.5"
}
},
"node_modules/yaeti": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
"integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==",
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
"license": "MIT",
"engines": {
"node": ">=0.10.32"
}
}
}
}
{
"name": "@skillhq/telegram",
"version": "0.10.0",
"description": "Fast Telegram CLI for reading, searching, and sending messages",
"type": "module",
"main": "dist/index.js",
"bin": {
"telegram": "dist/index.js"
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"start": "node dist/index.js",
"prepublishOnly": "npm run build"
},
"keywords": [
"telegram",
"cli",
"messaging",
"chat"
],
"author": "Derek Rein",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/skillhq/telegram.git"
},
"homepage": "https://github.com/skillhq/telegram#readme",
"bugs": {
"url": "https://github.com/skillhq/telegram/issues"
},
"dependencies": {
"big-integer": "^1.6.52",
"chalk": "^5.3.0",
"commander": "^14.0.0",
"json5": "^2.2.3",
"ora": "^8.1.0",
"telegram": "^2.26.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.7.0"
},
"engines": {
"node": ">=18.0.0"
},
"files": [
"dist",
"SKILL.md"
]
}
telegram - Telegram CLI
Fast Telegram CLI for reading, searching, and sending messages. Designed for both interactive use and AI agent integration.
Installation
Quick install (with onboarding)
curl -sSfL https://github.com/skillhq/telegram/raw/main/install.sh | bashThis installs the CLI, offers to set up the Telegram skill for AI agents, authenticates, and runs a test query.
As a Claude Code Skill (recommended)
npx skills add https://github.com/skillhq/telegram --skill telegramGlobal npm install
npm install -g @skillhq/telegramFrom source
git clone https://github.com/skillhq/telegram.git
cd telegram
npm install
npm run build
npm linkAuthentication
First, get your API credentials: 1. Go to https://my.telegram.org/apps 2. Log in with your phone number 3. Create a new application 4. Copy the api_id and api_hash
Then authenticate:
telegram authCommands
Auth & Status
telegram whoami # Show logged-in account
telegram check # Verify session/credentialsReading
telegram chats # List all chats
telegram chats --type group # Filter by type (user, group, supergroup, channel)
telegram read "MetaDAO Community" -n 50 # Read last 50 messages
telegram read "MetaDAO" --since "1h" # Messages from last hour
telegram read @username -n 20 # Read DM with user
telegram search "futarchy" --chat "MetaDAO" # Search within chat
telegram search "futarchy" --all # Search all chats
telegram inbox # Unread messages summaryWriting
telegram send @username "Hello" # Send DM
telegram send "GroupName" "Hello everyone" # Send to group
telegram reply "ChatName" 12345 "Response" # Reply to message IDContacts & Groups
telegram contact @username # Get contact info
telegram members "GroupName" # List group members
telegram admins "GroupName" # List admins only
telegram groups # List all groups
telegram groups --admin # Groups where you're admin
telegram kick "GroupName" @username # Remove a user from a group
telegram promote "GroupName" @username # Promote a member to admin
telegram transfer-owner "GroupName" @username # Transfer ownership (prompts for 2FA password)transfer-owner is irreversible and requires two-step verification on your account.It works on supergroups/channels only, prompts for your 2FA password securely, and asks
you to retype the group name to confirm (-y skips the confirmation).Muting
telegram mute "ChatName" # Mute forever
telegram mute "ChatName" -d 1h # Mute for 1 hour
telegram mute @username -d 8h # Mute DM for 8 hours
telegram unmute "ChatName" # UnmuteFolders
telegram folders # List all folders
telegram folder "Work" # Show chats in folder
telegram folder-add "Work" "ProjectChat" # Add chat to folder
telegram folder-remove "Work" "ProjectChat" # Remove chat from folderUtilities
telegram sync --days 7 # Sync last 7 days to markdown
telegram sync --chat "MetaDAO" --days 30 # Sync specific chatOutput Formats
All read commands support multiple output formats:
telegram chats --json # JSON (for scripts/AI)
telegram read "Chat" --markdown # Markdown format
telegram inbox --plain # Plain text (no colors)Configuration
Configuration is stored in ~/.config/tg/:
config.json- API credentials and session- Session data is encrypted and stored securely
Development
npm install
npm run build
npm run dev # Watch modeLicense
MIT
#!/usr/bin/env bash
set -euo pipefail
# Publish tg skill to ClawdHub
# Usage:
# ./scripts/publish-skill.sh # Use version from package.json
# ./scripts/publish-skill.sh 0.2.0 # Specify version
# ./scripts/publish-skill.sh patch # Bump patch (0.1.0 -> 0.1.1)
# ./scripts/publish-skill.sh minor # Bump minor (0.1.0 -> 0.2.0)
# ./scripts/publish-skill.sh major # Bump major (0.1.0 -> 1.0.0)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
cd "$PROJECT_DIR"
# Read current version from package.json
CURRENT_VERSION=$(node -p "require('./package.json').version")
# Determine target version
if [[ $# -eq 0 ]]; then
VERSION="$CURRENT_VERSION"
elif [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
VERSION="$1"
elif [[ "$1" == "patch" || "$1" == "minor" || "$1" == "major" ]]; then
IFS='.' read -r major minor patch <<< "$CURRENT_VERSION"
case "$1" in
patch) VERSION="$major.$minor.$((patch + 1))" ;;
minor) VERSION="$major.$((minor + 1)).0" ;;
major) VERSION="$((major + 1)).0.0" ;;
esac
else
echo "Usage: $0 [version|patch|minor|major]"
echo " version: semver like 0.2.0"
echo " patch: bump patch version"
echo " minor: bump minor version"
echo " major: bump major version"
exit 1
fi
# Get changelog from git (commits since last tag, or last 5 commits)
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [[ -n "$LAST_TAG" ]]; then
CHANGELOG=$(git log "$LAST_TAG"..HEAD --oneline --no-decorate | head -10 | sed 's/^/- /')
else
CHANGELOG=$(git log -5 --oneline --no-decorate | sed 's/^/- /')
fi
if [[ -z "$CHANGELOG" ]]; then
CHANGELOG="Version $VERSION"
fi
echo "Publishing tg skill v$VERSION"
echo "Changelog:"
echo "$CHANGELOG"
echo ""
# Update package.json version if different
if [[ "$VERSION" != "$CURRENT_VERSION" ]]; then
echo "Updating package.json version: $CURRENT_VERSION -> $VERSION"
npm pkg set version="$VERSION"
fi
# Publish to ClawdHub
# Note: --workdir is required to override clawdbot default workspace
TAGS="latest,telegram,messaging,chat,cli"
clawdhub --workdir "$PROJECT_DIR" publish . \
--slug tg \
--name "Telegram CLI" \
--version "$VERSION" \
--tags "$TAGS" \
--changelog "$CHANGELOG"
echo ""
echo "Published tg@$VERSION to ClawdHub"
import { appendFileSync, existsSync, mkdirSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
export interface AuditEntry {
timestamp: string;
command: string;
target: string;
message?: string;
replyToMsgId?: number;
kickedUser?: string;
targetUser?: string;
result: {
success: boolean;
messageId?: number;
error?: string;
};
}
const AUDIT_PATH = join(homedir(), '.config', 'tg', 'audit.jsonl');
export function auditLog(entry: AuditEntry): void {
try {
const dir = join(homedir(), '.config', 'tg');
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true, mode: 0o700 });
}
appendFileSync(AUDIT_PATH, JSON.stringify(entry) + '\n', { encoding: 'utf8', mode: 0o600 });
} catch {
// Never throw — audit failures must not break the caller
}
}
import { TelegramClient } from 'telegram';
import { StringSession } from 'telegram/sessions/index.js';
import { setCredentials, setSessionString } from './config.js';
import { prompt } from './prompt.js';
export async function authenticate(): Promise<TelegramClient> {
console.log('\nTelegram Authentication Setup\n');
console.log('To get your API credentials:');
console.log('1. Go to https://my.telegram.org/apps');
console.log('2. Log in with your phone number');
console.log('3. Create a new application (if you haven\'t already)');
console.log('4. Copy the api_id and api_hash\n');
const apiIdStr = await prompt('Enter your API ID: ');
const apiId = parseInt(apiIdStr, 10);
if (isNaN(apiId)) {
throw new Error('Invalid API ID');
}
const apiHash = await prompt('Enter your API Hash: ');
if (!apiHash) {
throw new Error('Invalid API Hash');
}
// Save credentials
setCredentials(apiId, apiHash);
console.log('\nConnecting to Telegram...');
const session = new StringSession('');
const client = new TelegramClient(session, apiId, apiHash, {
connectionRetries: 5,
});
await client.start({
phoneNumber: async () => await prompt('Enter your phone number (with country code, e.g., +1234567890): '),
password: async () => await prompt('Enter your 2FA password (press Enter if none): '),
phoneCode: async () => await prompt('Enter the code you received: '),
onError: (err) => console.error('Error:', err),
});
// Save session
const sessionString = (client.session as StringSession).save();
setSessionString(sessionString);
console.log('\nAuthentication successful! Session saved.');
return client;
}
export async function checkAuth(client: TelegramClient): Promise<boolean> {
try {
await client.connect();
return await client.isUserAuthorized();
} catch {
return false;
}
}
import { TelegramClient, Api } from 'telegram';
import { StringSession } from 'telegram/sessions/index.js';
import { computeCheck } from 'telegram/Password.js';
import { getCredentials, getSessionString, setSessionString, isConfigured } from './config.js';
import bigInt from 'big-integer';
let clientInstance: TelegramClient | null = null;
export async function getClient(): Promise<TelegramClient> {
if (clientInstance?.connected) {
return clientInstance;
}
if (!isConfigured()) {
throw new Error('Not configured. Run "tg auth" first to set up your API credentials.');
}
const { apiId, apiHash } = getCredentials();
const sessionString = getSessionString() || '';
const session = new StringSession(sessionString);
clientInstance = new TelegramClient(session, apiId, apiHash, {
connectionRetries: 5,
});
await clientInstance.connect();
if (!await clientInstance.isUserAuthorized()) {
throw new Error('Not authenticated. Run "tg auth" to log in.');
}
return clientInstance;
}
export async function createClient(apiId: number, apiHash: string): Promise<TelegramClient> {
const session = new StringSession('');
const client = new TelegramClient(session, apiId, apiHash, {
connectionRetries: 5,
});
await client.connect();
return client;
}
export async function saveSession(client: TelegramClient): Promise<void> {
const sessionString = (client.session as StringSession).save();
setSessionString(sessionString);
}
export async function disconnectClient(): Promise<void> {
if (clientInstance) {
await clientInstance.disconnect();
clientInstance = null;
}
}
export async function getMe(client: TelegramClient): Promise<Api.User> {
const me = await client.getMe();
if (!me || !(me instanceof Api.User)) {
throw new Error('Failed to get user info');
}
return me;
}
export interface ChatInfo {
id: string;
title: string;
type: 'user' | 'group' | 'supergroup' | 'channel';
username?: string;
unreadCount: number;
lastMessage?: string;
lastMessageDate?: Date;
}
export async function getDialogs(client: TelegramClient, limit = 100): Promise<ChatInfo[]> {
const dialogs = await client.getDialogs({ limit });
const chats: ChatInfo[] = [];
for (const dialog of dialogs) {
let type: ChatInfo['type'] = 'user';
let title = dialog.title || 'Unknown';
let username: string | undefined;
if (dialog.isUser) {
type = 'user';
const entity = dialog.entity as Api.User;
username = entity.username ?? undefined;
} else if (dialog.isGroup) {
type = 'group';
} else if (dialog.isChannel) {
const entity = dialog.entity as Api.Channel;
type = entity.megagroup ? 'supergroup' : 'channel';
username = entity.username ?? undefined;
}
chats.push({
id: dialog.id?.toString() || '',
title,
type,
username,
unreadCount: dialog.unreadCount,
lastMessage: dialog.message?.message,
lastMessageDate: dialog.message?.date ? new Date(dialog.message.date * 1000) : undefined,
});
}
return chats;
}
export interface MessageInfo {
id: number;
date: Date;
sender: string;
senderId?: string;
text: string;
replyToMsgId?: number;
isOutgoing: boolean;
mediaType?: string;
fileName?: string;
fileSize?: number;
}
export function parseTimeOffset(offset: string): Date {
const now = new Date();
const match = offset.match(/^(\d+)([mhd])$/);
if (!match) {
throw new Error(`Invalid time offset: ${offset}. Use format like "1h", "30m", "7d"`);
}
const value = parseInt(match[1]);
const unit = match[2];
switch (unit) {
case 'm': return new Date(now.getTime() - value * 60 * 1000);
case 'h': return new Date(now.getTime() - value * 60 * 60 * 1000);
case 'd': return new Date(now.getTime() - value * 24 * 60 * 60 * 1000);
default: throw new Error(`Unknown time unit: ${unit}`);
}
}
function getSenderCacheKey(fromId: Api.TypePeer): string {
if (fromId instanceof Api.PeerUser) return `user:${fromId.userId}`;
if (fromId instanceof Api.PeerChat) return `chat:${fromId.chatId}`;
if (fromId instanceof Api.PeerChannel) return `channel:${fromId.channelId}`;
return String(fromId);
}
function extractMediaInfo(media: Api.TypeMessageMedia | undefined): { mediaType?: string; fileName?: string; fileSize?: number } {
if (!media) return {};
if (media instanceof Api.MessageMediaPhoto) {
return { mediaType: 'photo' };
}
if (media instanceof Api.MessageMediaDocument && media.document instanceof Api.Document) {
const doc = media.document;
let mediaType = 'document';
let fileName: string | undefined;
for (const attr of doc.attributes) {
if (attr instanceof Api.DocumentAttributeFilename) {
fileName = attr.fileName;
}
if (attr instanceof Api.DocumentAttributeSticker) {
mediaType = 'sticker';
}
if (attr instanceof Api.DocumentAttributeVideo) {
mediaType = attr.roundMessage ? 'video_note' : 'video';
}
if (attr instanceof Api.DocumentAttributeAudio) {
mediaType = attr.voice ? 'voice' : 'audio';
}
if (attr instanceof Api.DocumentAttributeAnimated) {
mediaType = 'gif';
}
}
return { mediaType, fileName, fileSize: doc.size ? Number(doc.size) : undefined };
}
if (media instanceof Api.MessageMediaContact) {
return { mediaType: 'contact' };
}
if (media instanceof Api.MessageMediaGeo || media instanceof Api.MessageMediaGeoLive) {
return { mediaType: 'location' };
}
if (media instanceof Api.MessageMediaPoll) {
return { mediaType: 'poll' };
}
if (media instanceof Api.MessageMediaWebPage) {
return { mediaType: 'webpage' };
}
if (media instanceof Api.MessageMediaVenue) {
return { mediaType: 'venue' };
}
if (media instanceof Api.MessageMediaDice) {
return { mediaType: 'dice' };
}
return {};
}
export function formatMediaLabel(mediaType?: string, fileName?: string, fileSize?: number): string {
if (!mediaType) return '';
const sizeStr = fileSize ? ` (${formatFileSize(fileSize)})` : '';
switch (mediaType) {
case 'photo': return '📷 Photo';
case 'video': return `🎥 Video${sizeStr}`;
case 'video_note': return '🎥 Video note';
case 'voice': return '🎤 Voice message';
case 'audio': return fileName ? `🎵 ${fileName}${sizeStr}` : `🎵 Audio${sizeStr}`;
case 'document': return fileName ? `📎 ${fileName}${sizeStr}` : `📎 Document${sizeStr}`;
case 'sticker': return '😀 Sticker';
case 'gif': return '🎬 GIF';
case 'contact': return '👤 Contact';
case 'location': return '📍 Location';
case 'venue': return '📍 Venue';
case 'poll': return '📊 Poll';
case 'webpage': return '🔗 Link preview';
case 'dice': return '🎲 Dice';
default: return '📦 Media';
}
}
function formatFileSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
}
export async function getMessages(
client: TelegramClient,
chatIdentifier: string,
options: { limit?: number; offsetId?: number; minDate?: Date; maxDate?: Date; minId?: number } = {}
): Promise<{ messages: MessageInfo[]; chatTitle: string }> {
const { limit = 50, offsetId, minDate, maxDate, minId } = options;
const entity = await resolveChat(client, chatIdentifier);
const chatTitle = getChatTitle(entity);
const messages: MessageInfo[] = [];
const senderCache = new Map<string, { name: string; id: string }>();
const BATCH_SIZE = 100;
let currentOffsetId = offsetId;
let firstBatch = true;
while (messages.length < limit) {
const batchLimit = Math.min(BATCH_SIZE, limit - messages.length + 50);
const params: { limit: number; offsetId?: number; offsetDate?: number; minId?: number } = {
limit: batchLimit,
};
if (currentOffsetId) {
params.offsetId = currentOffsetId;
} else if (firstBatch && maxDate) {
// Server-side date filtering: start from maxDate on first batch
params.offsetDate = Math.floor(maxDate.getTime() / 1000);
}
if (minId) {
params.minId = minId;
}
const batch = await client.getMessages(entity, params);
firstBatch = false;
if (batch.length === 0) break;
let reachedMinDate = false;
for (const msg of batch) {
if (!(msg instanceof Api.Message)) continue;
const msgDate = new Date(msg.date * 1000);
if (maxDate && msgDate > maxDate) continue;
if (minDate && msgDate < minDate) {
reachedMinDate = true;
break;
}
if (messages.length >= limit) break;
// Resolve sender with cache
let sender = 'Unknown';
let senderId: string | undefined;
if (msg.fromId) {
const cacheKey = getSenderCacheKey(msg.fromId);
const cached = senderCache.get(cacheKey);
if (cached) {
sender = cached.name;
senderId = cached.id;
} else {
try {
const senderEntity = await client.getEntity(msg.fromId);
if (senderEntity instanceof Api.User) {
sender = senderEntity.firstName || senderEntity.username || 'Unknown';
senderId = senderEntity.id.toString();
} else if (senderEntity instanceof Api.Channel || senderEntity instanceof Api.Chat) {
sender = (senderEntity as Api.Channel | Api.Chat).title || 'Unknown';
senderId = senderEntity.id.toString();
}
senderCache.set(cacheKey, { name: sender, id: senderId || '' });
} catch {
// Ignore entity resolution errors
}
}
}
const mediaInfo = extractMediaInfo(msg.media);
messages.push({
id: msg.id,
date: msgDate,
sender,
senderId,
text: msg.message || '',
replyToMsgId: msg.replyTo?.replyToMsgId,
isOutgoing: msg.out ?? false,
...mediaInfo,
});
}
if (reachedMinDate) break;
if (messages.length >= limit) break;
// Set offsetId for next batch. Telegram mixes MessageService / MessageEmpty
// items into the raw batch (joins/leaves, pinned-msg markers, etc.). We
// must page by the oldest Api.Message in the batch - using raw
// batch[batch.length - 1] silently aborts pagination whenever the oldest
// raw item happens to be a service message, which for active channels
// usually kicks in within the first 5-10 iterations and caps deep
// backfills at a few days of history.
let oldestMessageInBatch: Api.Message | null = null;
for (let i = batch.length - 1; i >= 0; i--) {
const candidate = batch[i];
if (candidate instanceof Api.Message) {
oldestMessageInBatch = candidate;
break;
}
}
if (!oldestMessageInBatch) break; // batch was all service messages
if (currentOffsetId === oldestMessageInBatch.id) break; // no progress
currentOffsetId = oldestMessageInBatch.id;
// If batch was smaller than requested, no more messages
if (batch.length < batchLimit) break;
}
return { messages, chatTitle };
}
export async function searchMessages(
client: TelegramClient,
query: string,
options: { chat?: string; limit?: number } = {}
): Promise<{ messages: MessageInfo[]; chatTitle?: string }[]> {
const { chat, limit = 50 } = options;
const results: { messages: MessageInfo[]; chatTitle?: string }[] = [];
if (chat) {
const entity = await resolveChat(client, chat);
const chatTitle = getChatTitle(entity);
const searchResult = await client.invoke(
new Api.messages.Search({
peer: entity,
q: query,
filter: new Api.InputMessagesFilterEmpty(),
minDate: 0,
maxDate: 0,
offsetId: 0,
addOffset: 0,
limit,
maxId: 0,
minId: 0,
hash: bigInt(0),
})
);
const messages: MessageInfo[] = [];
if ('messages' in searchResult) {
for (const msg of searchResult.messages) {
if (msg instanceof Api.Message) {
let sender = 'Unknown';
if ('users' in searchResult) {
const user = searchResult.users.find(
(u): u is Api.User => u instanceof Api.User && u.id.equals(msg.fromId instanceof Api.PeerUser ? msg.fromId.userId : bigInt(0))
);
if (user) {
sender = user.firstName || user.username || 'Unknown';
}
}
messages.push({
id: msg.id,
date: new Date(msg.date * 1000),
sender,
text: msg.message || '',
replyToMsgId: msg.replyTo?.replyToMsgId,
isOutgoing: msg.out ?? false,
});
}
}
}
results.push({ messages, chatTitle });
} else {
// Global search
const searchResult = await client.invoke(
new Api.messages.SearchGlobal({
q: query,
filter: new Api.InputMessagesFilterEmpty(),
minDate: 0,
maxDate: 0,
offsetRate: 0,
offsetPeer: new Api.InputPeerEmpty(),
offsetId: 0,
limit,
})
);
const messages: MessageInfo[] = [];
if ('messages' in searchResult) {
for (const msg of searchResult.messages) {
if (msg instanceof Api.Message) {
messages.push({
id: msg.id,
date: new Date(msg.date * 1000),
sender: 'Unknown',
text: msg.message || '',
replyToMsgId: msg.replyTo?.replyToMsgId,
isOutgoing: msg.out ?? false,
});
}
}
}
results.push({ messages });
}
return results;
}
export async function sendMessage(
client: TelegramClient,
chatIdentifier: string,
text: string,
replyToMsgId?: number
): Promise<Api.Message> {
const entity = await resolveChat(client, chatIdentifier);
const result = await client.sendMessage(entity, {
message: text,
replyTo: replyToMsgId,
});
return result;
}
export async function getContactInfo(
client: TelegramClient,
identifier: string
): Promise<{
id: string;
firstName?: string;
lastName?: string;
username?: string;
phone?: string;
bio?: string;
isBot: boolean;
isMutualContact: boolean;
}> {
const entity = await client.getEntity(identifier);
if (!(entity instanceof Api.User)) {
throw new Error('Not a user');
}
let bio: string | undefined;
try {
const fullUser = await client.invoke(
new Api.users.GetFullUser({ id: entity })
);
bio = fullUser.fullUser.about ?? undefined;
} catch {
// Ignore
}
return {
id: entity.id.toString(),
firstName: entity.firstName ?? undefined,
lastName: entity.lastName ?? undefined,
username: entity.username ?? undefined,
phone: entity.phone ?? undefined,
bio,
isBot: entity.bot ?? false,
isMutualContact: entity.mutualContact ?? false,
};
}
export async function getChatMembers(
client: TelegramClient,
chatIdentifier: string,
options: { adminsOnly?: boolean; limit?: number } = {}
): Promise<{ id: string; name: string; username?: string; isAdmin: boolean }[]> {
const { adminsOnly = false, limit = 200 } = options;
const entity = await resolveChat(client, chatIdentifier);
if (entity instanceof Api.Channel) {
const filter = adminsOnly
? new Api.ChannelParticipantsAdmins()
: new Api.ChannelParticipantsRecent();
const result = await client.invoke(
new Api.channels.GetParticipants({
channel: entity,
filter,
offset: 0,
limit,
hash: bigInt(0),
})
);
if (!(result instanceof Api.channels.ChannelParticipants)) {
return [];
}
const members: { id: string; name: string; username?: string; isAdmin: boolean }[] = [];
for (const participant of result.participants) {
const userId = 'userId' in participant ? participant.userId : null;
if (!userId) continue;
const user = result.users.find(
(u): u is Api.User => u instanceof Api.User && u.id.equals(userId)
);
if (user) {
const isAdmin = participant instanceof Api.ChannelParticipantAdmin ||
participant instanceof Api.ChannelParticipantCreator;
members.push({
id: user.id.toString(),
name: [user.firstName, user.lastName].filter(Boolean).join(' ') || user.username || 'Unknown',
username: user.username ?? undefined,
isAdmin,
});
}
}
return members;
} else if (entity instanceof Api.Chat) {
const fullChat = await client.invoke(
new Api.messages.GetFullChat({ chatId: entity.id })
);
if (!('fullChat' in fullChat) || !(fullChat.fullChat instanceof Api.ChatFull)) {
return [];
}
const members: { id: string; name: string; username?: string; isAdmin: boolean }[] = [];
if (fullChat.fullChat.participants instanceof Api.ChatParticipants) {
for (const participant of fullChat.fullChat.participants.participants) {
const userId = participant.userId;
const user = fullChat.users.find(
(u): u is Api.User => u instanceof Api.User && u.id.equals(userId)
);
if (user) {
const isAdmin = participant instanceof Api.ChatParticipantAdmin ||
participant instanceof Api.ChatParticipantCreator;
if (!adminsOnly || isAdmin) {
members.push({
id: user.id.toString(),
name: [user.firstName, user.lastName].filter(Boolean).join(' ') || user.username || 'Unknown',
username: user.username ?? undefined,
isAdmin,
});
}
}
}
}
return members;
}
throw new Error('Not a group chat');
}
export async function getAdminGroups(client: TelegramClient): Promise<ChatInfo[]> {
const dialogs = await client.getDialogs({ limit: 500 });
const adminGroups: ChatInfo[] = [];
for (const dialog of dialogs) {
if (dialog.isGroup || dialog.isChannel) {
const entity = dialog.entity;
if (entity instanceof Api.Channel) {
if (entity.adminRights || entity.creator) {
adminGroups.push({
id: dialog.id?.toString() || '',
title: dialog.title || 'Unknown',
type: entity.megagroup ? 'supergroup' : 'channel',
username: entity.username ?? undefined,
unreadCount: dialog.unreadCount,
});
}
} else if (entity instanceof Api.Chat) {
// For regular groups, we need to check participants
try {
const fullChat = await client.invoke(
new Api.messages.GetFullChat({ chatId: entity.id })
);
const me = await client.getMe() as Api.User;
if ('fullChat' in fullChat && fullChat.fullChat instanceof Api.ChatFull) {
if (fullChat.fullChat.participants instanceof Api.ChatParticipants) {
const myParticipant = fullChat.fullChat.participants.participants.find(
p => p.userId.equals(me.id)
);
if (myParticipant instanceof Api.ChatParticipantAdmin ||
myParticipant instanceof Api.ChatParticipantCreator) {
adminGroups.push({
id: dialog.id?.toString() || '',
title: dialog.title || 'Unknown',
type: 'group',
unreadCount: dialog.unreadCount,
});
}
}
}
} catch {
// Skip if we can't get chat info
}
}
}
}
return adminGroups;
}
type ResolvedEntity = Api.User | Api.Chat | Api.Channel;
async function resolveChat(client: TelegramClient, identifier: string): Promise<ResolvedEntity> {
// Check if it's a username (starts with @)
if (identifier.startsWith('@')) {
const entity = await client.getEntity(identifier);
if (entity instanceof Api.User || entity instanceof Api.Chat || entity instanceof Api.Channel) {
return entity;
}
throw new Error(`Invalid entity type for: ${identifier}`);
}
// Try to find by exact name in dialogs
const dialogs = await client.getDialogs({ limit: 500 });
// First try exact match
let dialog = dialogs.find(d => d.title?.toLowerCase() === identifier.toLowerCase());
// Then try partial match
if (!dialog) {
dialog = dialogs.find(d => d.title?.toLowerCase().includes(identifier.toLowerCase()));
}
if (dialog && dialog.entity) {
const entity = dialog.entity;
if (entity instanceof Api.User || entity instanceof Api.Chat || entity instanceof Api.Channel) {
return entity;
}
}
// Try as a direct entity identifier
try {
const entity = await client.getEntity(identifier);
if (entity instanceof Api.User || entity instanceof Api.Chat || entity instanceof Api.Channel) {
return entity;
}
throw new Error(`Invalid entity type for: ${identifier}`);
} catch {
throw new Error(`Chat not found: ${identifier}`);
}
}
function getChatTitle(entity: ResolvedEntity): string {
if (entity instanceof Api.User) {
return entity.firstName || entity.username || 'Unknown';
}
if (entity instanceof Api.Chat || entity instanceof Api.Channel) {
return entity.title;
}
return 'Unknown';
}
// --- Mute/Unmute Functions ---
const MAX_INT32 = 2147483647;
export function parseDuration(duration: string): { seconds: number; isForever: boolean } {
if (duration === 'forever') {
return { seconds: 0, isForever: true };
}
const match = duration.match(/^(\d+)(m|h|d|w)$/);
if (!match) {
throw new Error(`Invalid duration format: ${duration}. Use formats like 1h, 8h, 1d, 1w, or "forever"`);
}
const value = parseInt(match[1], 10);
const unit = match[2];
let seconds: number;
switch (unit) {
case 'm': seconds = value * 60; break;
case 'h': seconds = value * 3600; break;
case 'd': seconds = value * 86400; break;
case 'w': seconds = value * 604800; break;
default: throw new Error(`Unknown duration unit: ${unit}`);
}
return { seconds, isForever: false };
}
// Keep for backward compatibility
export function parseDurationToSeconds(duration: string): number {
if (duration === 'forever') {
return MAX_INT32;
}
return parseDuration(duration).seconds;
}
export async function muteChat(
client: TelegramClient,
chatIdentifier: string,
duration: string = 'forever'
): Promise<{ success: boolean; message: string }> {
const chat = await resolveChat(client, chatIdentifier);
const chatTitle = getChatTitle(chat);
let inputPeer: Api.TypeInputNotifyPeer;
if (chat instanceof Api.User) {
inputPeer = new Api.InputNotifyPeer({
peer: new Api.InputPeerUser({ userId: chat.id, accessHash: chat.accessHash || bigInt(0) })
});
} else if (chat instanceof Api.Chat) {
inputPeer = new Api.InputNotifyPeer({
peer: new Api.InputPeerChat({ chatId: chat.id })
});
} else if (chat instanceof Api.Channel) {
inputPeer = new Api.InputNotifyPeer({
peer: new Api.InputPeerChannel({ channelId: chat.id, accessHash: chat.accessHash || bigInt(0) })
});
} else {
return { success: false, message: 'Unknown chat type' };
}
try {
const parsed = parseDuration(duration);
// For "forever", use MAX_INT32 directly; otherwise add seconds to current time
const muteUntil = parsed.isForever
? MAX_INT32
: Math.floor(Date.now() / 1000) + parsed.seconds;
await client.invoke(
new Api.account.UpdateNotifySettings({
peer: inputPeer,
settings: new Api.InputPeerNotifySettings({
muteUntil,
}),
})
);
const durationText = duration === 'forever' ? 'forever' : `for ${duration}`;
return { success: true, message: `Muted "${chatTitle}" ${durationText}` };
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return { success: false, message: msg };
}
}
export async function unmuteChat(
client: TelegramClient,
chatIdentifier: string
): Promise<{ success: boolean; message: string }> {
const chat = await resolveChat(client, chatIdentifier);
const chatTitle = getChatTitle(chat);
let inputPeer: Api.TypeInputNotifyPeer;
if (chat instanceof Api.User) {
inputPeer = new Api.InputNotifyPeer({
peer: new Api.InputPeerUser({ userId: chat.id, accessHash: chat.accessHash || bigInt(0) })
});
} else if (chat instanceof Api.Chat) {
inputPeer = new Api.InputNotifyPeer({
peer: new Api.InputPeerChat({ chatId: chat.id })
});
} else if (chat instanceof Api.Channel) {
inputPeer = new Api.InputNotifyPeer({
peer: new Api.InputPeerChannel({ channelId: chat.id, accessHash: chat.accessHash || bigInt(0) })
});
} else {
return { success: false, message: 'Unknown chat type' };
}
try {
await client.invoke(
new Api.account.UpdateNotifySettings({
peer: inputPeer,
settings: new Api.InputPeerNotifySettings({
muteUntil: 0,
}),
})
);
return { success: true, message: `Unmuted "${chatTitle}"` };
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return { success: false, message: msg };
}
}
// --- Folder Functions ---
export interface FolderInfo {
id: number;
title: string;
includedChats: { id: string; title: string; type: string }[];
excludedChats: { id: string; title: string; type: string }[];
emoticon?: string;
}
export async function getFolders(client: TelegramClient): Promise<FolderInfo[]> {
const result = await client.invoke(new Api.messages.GetDialogFilters());
const folders: FolderInfo[] = [];
for (const filter of result.filters) {
if (filter instanceof Api.DialogFilter) {
const includedChats: { id: string; title: string; type: string }[] = [];
const excludedChats: { id: string; title: string; type: string }[] = [];
// Resolve included peers
for (const peer of filter.includePeers) {
try {
const entity = await client.getEntity(peer);
if (entity instanceof Api.User || entity instanceof Api.Chat || entity instanceof Api.Channel) {
includedChats.push({
id: entity.id.toString(),
title: getChatTitleFromEntity(entity),
type: getEntityType(entity),
});
}
} catch {
// Skip unresolvable peers
}
}
// Resolve excluded peers
for (const peer of filter.excludePeers) {
try {
const entity = await client.getEntity(peer);
if (entity instanceof Api.User || entity instanceof Api.Chat || entity instanceof Api.Channel) {
excludedChats.push({
id: entity.id.toString(),
title: getChatTitleFromEntity(entity),
type: getEntityType(entity),
});
}
} catch {
// Skip unresolvable peers
}
}
// Handle title which can be string or TextWithEntities
const titleStr = typeof filter.title === 'string' ? filter.title : (filter.title?.text ?? 'Untitled');
folders.push({
id: filter.id,
title: titleStr,
includedChats,
excludedChats,
emoticon: filter.emoticon ?? undefined,
});
}
}
return folders;
}
export async function getFolder(
client: TelegramClient,
folderName: string
): Promise<FolderInfo | null> {
const folders = await getFolders(client);
return folders.find(f => f.title.toLowerCase() === folderName.toLowerCase()) || null;
}
export async function addChatToFolder(
client: TelegramClient,
folderName: string,
chatIdentifier: string
): Promise<{ success: boolean; message: string }> {
const result = await client.invoke(new Api.messages.GetDialogFilters());
let targetFilter: Api.DialogFilter | null = null;
for (const filter of result.filters) {
if (filter instanceof Api.DialogFilter) {
const filterTitle = typeof filter.title === 'string' ? filter.title : (filter.title?.text ?? '');
if (filterTitle.toLowerCase() === folderName.toLowerCase()) {
targetFilter = filter;
break;
}
}
}
if (!targetFilter) {
return { success: false, message: `Folder not found: ${folderName}` };
}
const chat = await resolveChat(client, chatIdentifier);
const chatTitle = getChatTitle(chat);
const folderTitle = typeof targetFilter.title === 'string' ? targetFilter.title : (targetFilter.title?.text ?? 'Untitled');
// Create the input peer for the chat
let inputPeer: Api.TypeInputPeer;
if (chat instanceof Api.User) {
inputPeer = new Api.InputPeerUser({ userId: chat.id, accessHash: chat.accessHash || bigInt(0) });
} else if (chat instanceof Api.Chat) {
inputPeer = new Api.InputPeerChat({ chatId: chat.id });
} else if (chat instanceof Api.Channel) {
inputPeer = new Api.InputPeerChannel({ channelId: chat.id, accessHash: chat.accessHash || bigInt(0) });
} else {
return { success: false, message: 'Unknown chat type' };
}
// Check if already included
for (const peer of targetFilter.includePeers) {
try {
const entity = await client.getEntity(peer);
if (entity.id.equals(chat.id)) {
return { success: false, message: `"${chatTitle}" is already in folder "${folderTitle}"` };
}
} catch {
// Skip
}
}
// Add to includePeers
const newIncludePeers = [...targetFilter.includePeers, inputPeer];
try {
await client.invoke(
new Api.messages.UpdateDialogFilter({
id: targetFilter.id,
filter: new Api.DialogFilter({
id: targetFilter.id,
title: targetFilter.title,
pinnedPeers: targetFilter.pinnedPeers,
includePeers: newIncludePeers,
excludePeers: targetFilter.excludePeers,
contacts: targetFilter.contacts,
nonContacts: targetFilter.nonContacts,
groups: targetFilter.groups,
broadcasts: targetFilter.broadcasts,
bots: targetFilter.bots,
excludeMuted: targetFilter.excludeMuted,
excludeRead: targetFilter.excludeRead,
excludeArchived: targetFilter.excludeArchived,
emoticon: targetFilter.emoticon,
}),
})
);
return { success: true, message: `Added "${chatTitle}" to folder "${folderTitle}"` };
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return { success: false, message: msg };
}
}
export async function removeChatFromFolder(
client: TelegramClient,
folderName: string,
chatIdentifier: string
): Promise<{ success: boolean; message: string }> {
const result = await client.invoke(new Api.messages.GetDialogFilters());
let targetFilter: Api.DialogFilter | null = null;
for (const filter of result.filters) {
if (filter instanceof Api.DialogFilter) {
const filterTitle = typeof filter.title === 'string' ? filter.title : (filter.title?.text ?? '');
if (filterTitle.toLowerCase() === folderName.toLowerCase()) {
targetFilter = filter;
break;
}
}
}
if (!targetFilter) {
return { success: false, message: `Folder not found: ${folderName}` };
}
const chat = await resolveChat(client, chatIdentifier);
const chatTitle = getChatTitle(chat);
const folderTitle = typeof targetFilter.title === 'string' ? targetFilter.title : (targetFilter.title?.text ?? 'Untitled');
// Find and remove from includePeers
let found = false;
const newIncludePeers: Api.TypeInputPeer[] = [];
for (const peer of targetFilter.includePeers) {
try {
const entity = await client.getEntity(peer);
if (entity.id.equals(chat.id)) {
found = true;
continue; // Skip this peer (remove it)
}
} catch {
// Keep unresolvable peers
}
newIncludePeers.push(peer);
}
if (!found) {
return { success: false, message: `"${chatTitle}" is not in folder "${folderTitle}"` };
}
try {
await client.invoke(
new Api.messages.UpdateDialogFilter({
id: targetFilter.id,
filter: new Api.DialogFilter({
id: targetFilter.id,
title: targetFilter.title,
pinnedPeers: targetFilter.pinnedPeers,
includePeers: newIncludePeers,
excludePeers: targetFilter.excludePeers,
contacts: targetFilter.contacts,
nonContacts: targetFilter.nonContacts,
groups: targetFilter.groups,
broadcasts: targetFilter.broadcasts,
bots: targetFilter.bots,
excludeMuted: targetFilter.excludeMuted,
excludeRead: targetFilter.excludeRead,
excludeArchived: targetFilter.excludeArchived,
emoticon: targetFilter.emoticon,
}),
})
);
return { success: true, message: `Removed "${chatTitle}" from folder "${folderTitle}"` };
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return { success: false, message: msg };
}
}
function getChatTitleFromEntity(entity: Api.User | Api.Chat | Api.Channel | Api.ChatForbidden | Api.ChannelForbidden): string {
if (entity instanceof Api.User) {
return entity.firstName || entity.username || 'Unknown User';
}
if (entity instanceof Api.Chat || entity instanceof Api.Channel) {
return entity.title;
}
if (entity instanceof Api.ChatForbidden || entity instanceof Api.ChannelForbidden) {
return entity.title;
}
return 'Unknown';
}
function getEntityType(entity: Api.User | Api.Chat | Api.Channel | Api.ChatForbidden | Api.ChannelForbidden): string {
if (entity instanceof Api.User) return 'user';
if (entity instanceof Api.Chat) return 'group';
if (entity instanceof Api.Channel) return entity.megagroup ? 'supergroup' : 'channel';
return 'unknown';
}
// --- Kick Function ---
export async function kickUser(
client: TelegramClient,
chatIdentifier: string,
userIdentifier: string
): Promise<{ success: boolean; message: string }> {
const chat = await resolveChat(client, chatIdentifier);
// Get the user to kick
let user: Api.User;
try {
const entity = await client.getEntity(userIdentifier);
if (!(entity instanceof Api.User)) {
return { success: false, message: 'Target is not a user' };
}
user = entity;
} catch (e) {
return { success: false, message: `User not found: ${userIdentifier}` };
}
if (chat instanceof Api.Channel) {
// For channels/supergroups, use EditBanned
try {
await client.invoke(
new Api.channels.EditBanned({
channel: chat,
participant: user,
bannedRights: new Api.ChatBannedRights({
untilDate: 0, // Permanent
viewMessages: true,
sendMessages: true,
sendMedia: true,
sendStickers: true,
sendGifs: true,
sendGames: true,
sendInline: true,
embedLinks: true,
}),
})
);
return { success: true, message: `Kicked ${user.username || user.firstName} from ${chat.title}` };
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
if (msg.includes('ADMIN') || msg.includes('RIGHT')) {
return { success: false, message: 'Not admin or insufficient rights' };
}
if (msg.includes('USER_NOT_PARTICIPANT')) {
return { success: false, message: 'User is not a member' };
}
return { success: false, message: msg };
}
} else if (chat instanceof Api.Chat) {
// For regular groups, use DeleteChatUser
try {
await client.invoke(
new Api.messages.DeleteChatUser({
chatId: chat.id,
userId: user,
revokeHistory: false,
})
);
return { success: true, message: `Kicked ${user.username || user.firstName} from ${chat.title}` };
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
if (msg.includes('ADMIN') || msg.includes('RIGHT')) {
return { success: false, message: 'Not admin or insufficient rights' };
}
if (msg.includes('USER_NOT_PARTICIPANT')) {
return { success: false, message: 'User is not a member' };
}
return { success: false, message: msg };
}
}
return { success: false, message: 'Not a group chat' };
}
// --- Admin Management Functions ---
async function resolveUser(client: TelegramClient, userIdentifier: string): Promise<Api.User> {
const entity = await client.getEntity(userIdentifier);
if (!(entity instanceof Api.User)) {
throw new Error('Target is not a user');
}
return entity;
}
function userLabel(user: Api.User): string {
return user.username ? `@${user.username}` : (user.firstName || user.id.toString());
}
function friendlyAdminError(e: unknown): string {
const msg = e instanceof Error ? e.message : String(e);
if (msg.includes('USER_NOT_PARTICIPANT') || msg.includes('USER_NOT_MUTUAL_CONTACT')) {
return 'The target must be a member of the group first.';
}
if (msg.includes('CHAT_ADMIN_REQUIRED') || msg.includes('ADMIN_RIGHT')) {
return 'You lack the rights to add admins in this group.';
}
if (msg.includes('USER_PRIVACY_RESTRICTED')) {
return "The target's privacy settings prevent this action.";
}
if (msg.includes('USER_CREATOR')) {
return 'That user is the group creator and cannot be changed this way.';
}
if (msg.includes('USER_ADMIN_INVALID')) {
return "You can't edit this user's admin status (they may have been promoted by someone else).";
}
if (msg.includes('RIGHT_FORBIDDEN')) {
return 'One of the requested admin rights is not allowed in this group.';
}
if (msg.includes('ADMINS_TOO_MUCH')) {
return 'This group already has the maximum number of admins.';
}
return msg;
}
function friendlyTransferError(e: unknown): string {
const msg = e instanceof Error ? e.message : String(e);
if (msg.includes('PASSWORD_HASH_INVALID')) {
return 'Incorrect 2FA password.';
}
if (msg.includes('PASSWORD_MISSING')) {
return 'No 2FA password is set on your account. Enable two-step verification first.';
}
if (msg.includes('SRP_PASSWORD_CHANGED') || msg.includes('SRP_ID_INVALID')) {
return 'Your 2FA password state changed mid-request. Please try again.';
}
if (msg.includes('PASSWORD_TOO_FRESH')) {
return 'Your 2FA password was set too recently. Telegram blocks ownership transfer for ~7 days after enabling or changing it.';
}
if (msg.includes('SESSION_TOO_FRESH') || msg.includes('FRESH_CHANGE_ADMINS_FORBIDDEN')) {
return 'This login session is too new. Telegram blocks ownership transfer for ~24h after a new login.';
}
if (msg.includes('CHAT_ADMIN_REQUIRED') || msg.includes('CHANNEL_PRIVATE')) {
return 'You must be the creator of this group to transfer ownership.';
}
if (msg.includes('USER_NOT_PARTICIPANT') || msg.includes('USER_NOT_MUTUAL_CONTACT')) {
return 'The target must be a member of the group first.';
}
if (msg.includes('USER_PRIVACY_RESTRICTED')) {
return "The target's privacy settings prevent the transfer. Ask them to adjust their privacy settings or add you as a contact.";
}
if (msg.includes('USER_CHANNELS_TOO_MUCH')) {
return 'The target is in too many groups/channels and cannot receive ownership right now.';
}
if (msg.includes('CHANNELS_ADMIN_PUBLIC_TOO_MUCH')) {
return 'The target already owns too many public groups/channels.';
}
return msg;
}
export async function promoteAdmin(
client: TelegramClient,
chatIdentifier: string,
userIdentifier: string,
options: { rank?: string; canAddAdmins?: boolean } = {}
): Promise<{ success: boolean; message: string }> {
const chat = await resolveChat(client, chatIdentifier);
let user: Api.User;
try {
user = await resolveUser(client, userIdentifier);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
if (msg === 'Target is not a user') {
return { success: false, message: msg };
}
return { success: false, message: `User not found: ${userIdentifier}` };
}
if (chat instanceof Api.Channel) {
try {
await client.invoke(
new Api.channels.EditAdmin({
channel: chat,
userId: user,
adminRights: new Api.ChatAdminRights({
changeInfo: true,
postMessages: true,
editMessages: true,
deleteMessages: true,
banUsers: true,
inviteUsers: true,
pinMessages: true,
manageCall: true,
other: true,
addAdmins: options.canAddAdmins ?? false,
}),
rank: options.rank ?? '',
})
);
return { success: true, message: `Promoted ${userLabel(user)} to admin in "${chat.title}"` };
} catch (e: unknown) {
return { success: false, message: friendlyAdminError(e) };
}
} else if (chat instanceof Api.Chat) {
// Basic groups only support a single all-or-nothing admin toggle.
try {
await client.invoke(
new Api.messages.EditChatAdmin({
chatId: chat.id,
userId: user,
isAdmin: true,
})
);
const ignoredNote = (options.rank || options.canAddAdmins)
? ' (basic groups grant full admin rights; --rank/--add-admins ignored)'
: '';
return { success: true, message: `Promoted ${userLabel(user)} to admin in "${chat.title}"${ignoredNote}` };
} catch (e: unknown) {
return { success: false, message: friendlyAdminError(e) };
}
}
return { success: false, message: 'Not a group chat' };
}
export async function transferOwnership(
client: TelegramClient,
chatIdentifier: string,
userIdentifier: string,
password: string
): Promise<{ success: boolean; message: string }> {
const chat = await resolveChat(client, chatIdentifier);
if (!(chat instanceof Api.Channel)) {
return {
success: false,
message: 'Ownership transfer is only supported for supergroups and channels. Convert a basic group to a supergroup first.',
};
}
let user: Api.User;
try {
user = await resolveUser(client, userIdentifier);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
if (msg === 'Target is not a user') {
return { success: false, message: msg };
}
return { success: false, message: `User not found: ${userIdentifier}` };
}
let passwordInfo: Api.account.Password;
try {
passwordInfo = await client.invoke(new Api.account.GetPassword());
} catch (e: unknown) {
return { success: false, message: e instanceof Error ? e.message : String(e) };
}
if (!passwordInfo.hasPassword) {
return {
success: false,
message: 'Ownership transfer requires two-step verification (a cloud password) on your account. Enable it in Telegram > Settings > Privacy and Security first.',
};
}
try {
const srpCheck = await computeCheck(passwordInfo, password);
await client.invoke(
new Api.channels.EditCreator({
channel: chat,
userId: user,
password: srpCheck,
})
);
return { success: true, message: `Transferred ownership of "${chat.title}" to ${userLabel(user)}` };
} catch (e: unknown) {
return { success: false, message: friendlyTransferError(e) };
}
}
import { Command } from 'commander';
import { authenticate } from '../auth.js';
import { isConfigured, saveConfig } from '../config.js';
import ora from 'ora';
export const authCommand = new Command('auth')
.description('Authenticate with Telegram')
.option('--op-vault <vault>', 'Set 1Password vault for secret storage')
.action(async (opts: { opVault?: string }) => {
// Save op vault config if provided (before auth, so secrets route there)
if (opts.opVault) {
saveConfig({ opVault: opts.opVault });
console.log(`1Password vault set to: ${opts.opVault}`);
}
if (isConfigured()) {
console.log('Already configured. Run "tg check" to verify your session.');
console.log('To re-authenticate, delete ~/.config/tg/config.json5 and run "tg auth" again.');
return;
}
try {
const client = await authenticate();
await client.disconnect();
} catch (error) {
console.error('Authentication failed:', error instanceof Error ? error.message : error);
process.exit(1);
}
});
import { Command } from 'commander';
import { getClient, getDialogs, disconnectClient } from '../client.js';
import { formatJson } from '../formatters/json.js';
import { formatChats } from '../formatters/plain.js';
import { formatChatsMarkdown } from '../formatters/markdown.js';
import { getOutputFormat } from '../formatters/index.js';
import ora from 'ora';
export const chatsCommand = new Command('chats')
.description('List all chats')
.option('-n, --limit <number>', 'Maximum number of chats', '100')
.option('--json', 'Output as JSON')
.option('--markdown', 'Output as Markdown')
.option('--type <type>', 'Filter by type: user, group, supergroup, channel')
.action(async (options) => {
const spinner = ora('Fetching chats...').start();
try {
const client = await getClient();
let chats = await getDialogs(client, parseInt(options.limit));
if (options.type) {
chats = chats.filter(c => c.type === options.type);
}
spinner.stop();
const format = getOutputFormat(options);
switch (format) {
case 'json':
console.log(formatJson(chats));
break;
case 'markdown':
console.log(formatChatsMarkdown(chats));
break;
default:
console.log(formatChats(chats));
}
await disconnectClient();
} catch (error) {
spinner.fail('Failed to fetch chats');
console.error(error instanceof Error ? error.message : error);
process.exit(1);
}
});
import { Command } from 'commander';
import { getClient, getMe, disconnectClient } from '../client.js';
import { isConfigured } from '../config.js';
import { isSecretStoreAvailable } from '../secrets.js';
import { isOnePasswordAvailable } from '../onepassword.js';
import { isKeychainAvailable } from '../keychain.js';
import chalk from 'chalk';
import ora from 'ora';
function getSecretBackendLabel(): string {
if (isOnePasswordAvailable()) return '1Password';
if (isKeychainAvailable()) return 'macOS Keychain';
return 'config file (plaintext)';
}
export const checkCommand = new Command('check')
.description('Verify session and credentials')
.action(async () => {
if (!isConfigured()) {
console.log(chalk.red('Not configured. Run "tg auth" first.'));
process.exit(1);
}
const spinner = ora('Checking session...').start();
try {
const client = await getClient();
const me = await getMe(client);
spinner.succeed(chalk.green('Session valid'));
console.log(`Logged in as: ${me.firstName || ''} ${me.lastName || ''} (@${me.username || 'no username'})`);
console.log(`Secret storage: ${getSecretBackendLabel()}`);
await disconnectClient();
} catch (error) {
spinner.fail(chalk.red('Session invalid or expired'));
console.error(error instanceof Error ? error.message : error);
console.log('\nRun "tg auth" to re-authenticate.');
process.exit(1);
}
});
import { Command } from 'commander';
import { getClient, getContactInfo, disconnectClient } from '../client.js';
import { formatJson } from '../formatters/json.js';
import { formatContact } from '../formatters/plain.js';
import { formatContactMarkdown } from '../formatters/markdown.js';
import { getOutputFormat } from '../formatters/index.js';
import ora from 'ora';
export const contactCommand = new Command('contact')
.description('Get contact information')
.argument('<user>', 'Username (@user) or phone number')
.option('--json', 'Output as JSON')
.option('--markdown', 'Output as Markdown')
.action(async (user, options) => {
const spinner = ora(`Fetching contact info for "${user}"...`).start();
try {
const client = await getClient();
const contact = await getContactInfo(client, user);
spinner.stop();
const format = getOutputFormat(options);
switch (format) {
case 'json':
console.log(formatJson(contact));
break;
case 'markdown':
console.log(formatContactMarkdown(contact));
break;
default:
console.log(formatContact(contact));
}
await disconnectClient();
} catch (error) {
spinner.fail('Failed to fetch contact info');
console.error(error instanceof Error ? error.message : error);
process.exit(1);
}
});
import { Command } from 'commander';
import { getClient, addChatToFolder, disconnectClient } from '../client.js';
import { assertWriteEnabled } from '../guard.js';
import ora from 'ora';
export const folderAddCommand = new Command('folder-add')
.description('Add a chat to a folder')
.argument('<folder>', 'Folder name')
.argument('<chat>', 'Chat name, username, or ID')
.action(async (folder, chat) => {
assertWriteEnabled();
const spinner = ora(`Adding "${chat}" to folder "${folder}"...`).start();
try {
const client = await getClient();
const result = await addChatToFolder(client, folder, chat);
if (result.success) {
spinner.succeed(result.message);
} else {
spinner.fail(result.message);
process.exit(1);
}
await disconnectClient();
} catch (error) {
spinner.fail('Failed to add chat to folder');
console.error(error instanceof Error ? error.message : error);
process.exit(1);
}
});
import { Command } from 'commander';
import { getClient, removeChatFromFolder, disconnectClient } from '../client.js';
import { assertWriteEnabled } from '../guard.js';
import ora from 'ora';
export const folderRemoveCommand = new Command('folder-remove')
.description('Remove a chat from a folder')
.argument('<folder>', 'Folder name')
.argument('<chat>', 'Chat name, username, or ID')
.action(async (folder, chat) => {
assertWriteEnabled();
const spinner = ora(`Removing "${chat}" from folder "${folder}"...`).start();
try {
const client = await getClient();
const result = await removeChatFromFolder(client, folder, chat);
if (result.success) {
spinner.succeed(result.message);
} else {
spinner.fail(result.message);
process.exit(1);
}
await disconnectClient();
} catch (error) {
spinner.fail('Failed to remove chat from folder');
console.error(error instanceof Error ? error.message : error);
process.exit(1);
}
});
import { Command } from 'commander';
import { getClient, getFolder, disconnectClient } from '../client.js';
import { formatFolder } from '../formatters/plain.js';
import ora from 'ora';
export const folderCommand = new Command('folder')
.description('Show chats in a specific folder')
.argument('<name>', 'Folder name')
.option('--json', 'Output as JSON')
.action(async (name, options) => {
const spinner = ora(`Loading folder "${name}"...`).start();
try {
const client = await getClient();
const folder = await getFolder(client, name);
spinner.stop();
if (!folder) {
console.error(`Folder not found: ${name}`);
process.exit(1);
}
if (options.json) {
console.log(JSON.stringify(folder, null, 2));
} else {
console.log(formatFolder(folder));
}
await disconnectClient();
} catch (error) {
spinner.fail('Failed to load folder');
console.error(error instanceof Error ? error.message : error);
process.exit(1);
}
});
import { Command } from 'commander';
import { getClient, getFolders, disconnectClient } from '../client.js';
import { formatFolders } from '../formatters/plain.js';
import ora from 'ora';
export const foldersCommand = new Command('folders')
.description('List all folders with their chats')
.option('--json', 'Output as JSON')
.action(async (options) => {
const spinner = ora('Loading folders...').start();
try {
const client = await getClient();
const folders = await getFolders(client);
spinner.stop();
if (options.json) {
console.log(JSON.stringify(folders, null, 2));
} else {
if (folders.length === 0) {
console.log('No folders found.');
} else {
console.log(formatFolders(folders));
}
}
await disconnectClient();
} catch (error) {
spinner.fail('Failed to load folders');
console.error(error instanceof Error ? error.message : error);
process.exit(1);
}
});
import { Command } from 'commander';
import { getClient, getDialogs, getAdminGroups, disconnectClient } from '../client.js';
import { formatJson } from '../formatters/json.js';
import { formatChats } from '../formatters/plain.js';
import { formatChatsMarkdown } from '../formatters/markdown.js';
import { getOutputFormat } from '../formatters/index.js';
import ora from 'ora';
export const groupsCommand = new Command('groups')
.description('List groups')
.option('--admin', 'Only show groups where you are admin')
.option('--json', 'Output as JSON')
.option('--markdown', 'Output as Markdown')
.action(async (options) => {
const spinner = ora('Fetching groups...').start();
try {
const client = await getClient();
let groups;
if (options.admin) {
groups = await getAdminGroups(client);
} else {
const allChats = await getDialogs(client, 500);
groups = allChats.filter(c => c.type === 'group' || c.type === 'supergroup');
}
spinner.stop();
const format = getOutputFormat(options);
switch (format) {
case 'json':
console.log(formatJson(groups));
break;
case 'markdown':
console.log(formatChatsMarkdown(groups));
break;
default:
console.log(formatChats(groups));
}
await disconnectClient();
} catch (error) {
spinner.fail('Failed to fetch groups');
console.error(error instanceof Error ? error.message : error);
process.exit(1);
}
});
import { Command } from 'commander';
import { getClient, getDialogs, disconnectClient } from '../client.js';
import { formatJson } from '../formatters/json.js';
import { formatInbox } from '../formatters/plain.js';
import { formatInboxMarkdown } from '../formatters/markdown.js';
import { getOutputFormat } from '../formatters/index.js';
import ora from 'ora';
export const inboxCommand = new Command('inbox')
.description('Show unread messages summary')
.option('--json', 'Output as JSON')
.option('--markdown', 'Output as Markdown')
.action(async (options) => {
const spinner = ora('Fetching inbox...').start();
try {
const client = await getClient();
const chats = await getDialogs(client, 500);
const unreadChats = chats.filter(c => c.unreadCount > 0);
spinner.stop();
const format = getOutputFormat(options);
switch (format) {
case 'json':
console.log(formatJson({
totalUnread: unreadChats.reduce((sum, c) => sum + c.unreadCount, 0),
chatsWithUnread: unreadChats.length,
chats: unreadChats,
}));
break;
case 'markdown':
console.log(formatInboxMarkdown(chats));
break;
default:
console.log(formatInbox(chats));
}
await disconnectClient();
} catch (error) {
spinner.fail('Failed to fetch inbox');
console.error(error instanceof Error ? error.message : error);
process.exit(1);
}
});
export { authCommand } from './auth.js';
export { logoutCommand } from './logout.js';
export { whoamiCommand } from './whoami.js';
export { checkCommand } from './check.js';
export { chatsCommand } from './chats.js';
export { readCommand } from './read.js';
export { searchCommand } from './search.js';
export { sendCommand } from './send.js';
export { replyCommand } from './reply.js';
export { inboxCommand } from './inbox.js';
export { contactCommand } from './contact.js';
export { membersCommand, adminsCommand } from './members.js';
export { groupsCommand } from './groups.js';
export { syncCommand } from './sync.js';
export { kickCommand } from './kick.js';
export { promoteCommand } from './promote.js';
export { transferOwnerCommand } from './transfer-owner.js';
export { muteCommand } from './mute.js';
export { unmuteCommand } from './unmute.js';
export { foldersCommand } from './folders.js';
export { folderCommand } from './folder.js';
export { folderAddCommand } from './folder-add.js';
export { folderRemoveCommand } from './folder-remove.js';
export { writeAccessCommand } from './write-access.js';
import { Command } from 'commander';
import { getClient, kickUser, disconnectClient } from '../client.js';
import { auditLog } from '../audit.js';
import { assertWriteEnabled } from '../guard.js';
import ora from 'ora';
export const kickCommand = new Command('kick')
.description('Kick/remove a user from a group')
.argument('<group>', 'Group name or username')
.argument('<user>', 'Username to kick (e.g., @username)')
.action(async (group, user) => {
assertWriteEnabled();
const spinner = ora(`Kicking ${user} from "${group}"...`).start();
try {
const client = await getClient();
const result = await kickUser(client, group, user);
auditLog({ timestamp: new Date().toISOString(), command: 'kick', target: group, kickedUser: user, result: { success: result.success } });
if (result.success) {
spinner.succeed(result.message);
} else {
spinner.fail(result.message);
process.exit(1);
}
await disconnectClient();
} catch (error) {
auditLog({ timestamp: new Date().toISOString(), command: 'kick', target: group, kickedUser: user, result: { success: false, error: error instanceof Error ? error.message : String(error) } });
spinner.fail('Failed to kick user');
console.error(error instanceof Error ? error.message : error);
process.exit(1);
}
});
import { Command } from 'commander';
import { clearSessionString } from '../config.js';
import { auditLog } from '../audit.js';
import chalk from 'chalk';
import ora from 'ora';
export const logoutCommand = new Command('logout')
.description('Clear saved session (log out)')
.action(async () => {
const spinner = ora('Logging out...').start();
try {
clearSessionString();
auditLog({
timestamp: new Date().toISOString(),
command: 'logout',
target: '',
result: { success: true },
});
spinner.succeed(chalk.green('Logged out. Run "tg auth" to re-authenticate.'));
} catch (error) {
auditLog({
timestamp: new Date().toISOString(),
command: 'logout',
target: '',
result: { success: false, error: error instanceof Error ? error.message : String(error) },
});
spinner.fail(chalk.red('Failed to log out'));
console.error(error instanceof Error ? error.message : error);
process.exit(1);
}
});
import { Command } from 'commander';
import { getClient, getChatMembers, disconnectClient } from '../client.js';
import { formatJson } from '../formatters/json.js';
import { formatMembers } from '../formatters/plain.js';
import { formatMembersMarkdown } from '../formatters/markdown.js';
import { getOutputFormat } from '../formatters/index.js';
import ora from 'ora';
export const membersCommand = new Command('members')
.description('List group members')
.argument('<group>', 'Group name or username')
.option('-n, --limit <number>', 'Maximum members to fetch', '200')
.option('--json', 'Output as JSON')
.option('--markdown', 'Output as Markdown')
.action(async (group, options) => {
const spinner = ora(`Fetching members of "${group}"...`).start();
try {
const client = await getClient();
const members = await getChatMembers(client, group, {
limit: parseInt(options.limit),
});
spinner.stop();
const format = getOutputFormat(options);
switch (format) {
case 'json':
console.log(formatJson(members));
break;
case 'markdown':
console.log(formatMembersMarkdown(members));
break;
default:
console.log(formatMembers(members));
}
await disconnectClient();
} catch (error) {
spinner.fail('Failed to fetch members');
console.error(error instanceof Error ? error.message : error);
process.exit(1);
}
});
export const adminsCommand = new Command('admins')
.description('List group admins')
.argument('<group>', 'Group name or username')
.option('--json', 'Output as JSON')
.option('--markdown', 'Output as Markdown')
.action(async (group, options) => {
const spinner = ora(`Fetching admins of "${group}"...`).start();
try {
const client = await getClient();
const members = await getChatMembers(client, group, {
adminsOnly: true,
});
spinner.stop();
const format = getOutputFormat(options);
switch (format) {
case 'json':
console.log(formatJson(members));
break;
case 'markdown':
console.log(formatMembersMarkdown(members));
break;
default:
console.log(formatMembers(members));
}
await disconnectClient();
} catch (error) {
spinner.fail('Failed to fetch admins');
console.error(error instanceof Error ? error.message : error);
process.exit(1);
}
});
import { Command } from 'commander';
import { getClient, muteChat, disconnectClient } from '../client.js';
import { assertWriteEnabled } from '../guard.js';
import ora from 'ora';
export const muteCommand = new Command('mute')
.description('Mute a chat')
.argument('<chat>', 'Chat name, username, or ID')
.option('-d, --duration <duration>', 'Duration (1h, 8h, 1d, 1w, or forever)', 'forever')
.action(async (chat, options) => {
assertWriteEnabled();
const spinner = ora(`Muting "${chat}"...`).start();
try {
const client = await getClient();
const result = await muteChat(client, chat, options.duration);
if (result.success) {
spinner.succeed(result.message);
} else {
spinner.fail(result.message);
process.exit(1);
}
await disconnectClient();
} catch (error) {
spinner.fail('Failed to mute chat');
console.error(error instanceof Error ? error.message : error);
process.exit(1);
}
});
import { Command } from 'commander';
import { getClient, promoteAdmin, disconnectClient } from '../client.js';
import { auditLog } from '../audit.js';
import { assertWriteEnabled } from '../guard.js';
import ora from 'ora';
export const promoteCommand = new Command('promote')
.description('Promote a group member to admin')
.argument('<group>', 'Group name or @username')
.argument('<user>', 'Username to promote (e.g., @username)')
.option('--rank <title>', 'Custom admin title/badge (supergroups only)')
.option('--add-admins', 'Also allow the new admin to add other admins')
.action(async (group, user, options) => {
assertWriteEnabled();
const spinner = ora(`Promoting ${user} in "${group}"...`).start();
try {
const client = await getClient();
const result = await promoteAdmin(client, group, user, {
rank: options.rank,
canAddAdmins: options.addAdmins,
});
auditLog({ timestamp: new Date().toISOString(), command: 'promote', target: group, targetUser: user, result: { success: result.success, error: result.success ? undefined : result.message } });
if (result.success) {
spinner.succeed(result.message);
} else {
spinner.fail(result.message);
process.exit(1);
}
await disconnectClient();
} catch (error) {
auditLog({ timestamp: new Date().toISOString(), command: 'promote', target: group, targetUser: user, result: { success: false, error: error instanceof Error ? error.message : String(error) } });
spinner.fail('Failed to promote user');
console.error(error instanceof Error ? error.message : error);
process.exit(1);
}
});
import { Command } from 'commander';
import { getClient, getMessages, disconnectClient, parseTimeOffset } from '../client.js';
import { formatJson } from '../formatters/json.js';
import { formatMessages } from '../formatters/plain.js';
import { formatMessagesMarkdown } from '../formatters/markdown.js';
import { getOutputFormat } from '../formatters/index.js';
import ora from 'ora';
export const readCommand = new Command('read')
.description('Read messages from a chat')
.argument('<chat>', 'Chat name, username (@user), or ID')
.option('-n, --limit <number>', 'Number of messages to fetch', '50')
.option('--since <time>', 'Get messages since (e.g., "1h", "30m", "7d")')
.option('--until <time>', 'Get messages until (e.g., "1h", "30m", "7d")')
.option('--json', 'Output as JSON')
.option('--markdown', 'Output as Markdown')
.action(async (chat, options) => {
const spinner = ora(`Fetching messages from "${chat}"...`).start();
try {
const client = await getClient();
const fetchOptions: Parameters<typeof getMessages>[2] = {
limit: parseInt(options.limit),
};
if (options.since) {
fetchOptions.minDate = parseTimeOffset(options.since);
}
if (options.until) {
fetchOptions.maxDate = parseTimeOffset(options.until);
}
const { messages, chatTitle } = await getMessages(client, chat, fetchOptions);
spinner.stop();
const format = getOutputFormat(options);
switch (format) {
case 'json':
console.log(formatJson({ chatTitle, messages }));
break;
case 'markdown':
console.log(formatMessagesMarkdown(messages, chatTitle));
break;
default:
console.log(formatMessages(messages, chatTitle));
}
await disconnectClient();
} catch (error) {
spinner.fail('Failed to fetch messages');
console.error(error instanceof Error ? error.message : error);
process.exit(1);
}
});
import { Command } from 'commander';
import { getClient, sendMessage, disconnectClient } from '../client.js';
import { formatJson } from '../formatters/json.js';
import { auditLog } from '../audit.js';
import { assertWriteEnabled } from '../guard.js';
import chalk from 'chalk';
import ora from 'ora';
export const replyCommand = new Command('reply')
.description('Reply to a message')
.argument('<chat>', 'Chat name, username (@user), or ID')
.argument('<msg-id>', 'Message ID to reply to')
.argument('<message>', 'Reply text')
.option('--json', 'Output as JSON')
.action(async (chat, msgId, message, options) => {
assertWriteEnabled();
const spinner = ora('Sending reply...').start();
try {
const client = await getClient();
const result = await sendMessage(client, chat, message, parseInt(msgId));
auditLog({ timestamp: new Date().toISOString(), command: 'reply', target: chat, message, replyToMsgId: parseInt(msgId), result: { success: true, messageId: result.id } });
spinner.succeed(chalk.green('Reply sent'));
if (options.json) {
console.log(formatJson({
id: result.id,
replyToMsgId: parseInt(msgId),
date: result.date ? new Date(result.date * 1000).toISOString() : null,
text: result.message,
}));
} else {
console.log(chalk.gray(`Message ID: ${result.id}`));
}
await disconnectClient();
} catch (error) {
auditLog({ timestamp: new Date().toISOString(), command: 'reply', target: chat, message, replyToMsgId: parseInt(msgId), result: { success: false, error: error instanceof Error ? error.message : String(error) } });
spinner.fail('Failed to send reply');
console.error(error instanceof Error ? error.message : error);
process.exit(1);
}
});
import { Command } from 'commander';
import { getClient, searchMessages, disconnectClient } from '../client.js';
import { formatJson } from '../formatters/json.js';
import { formatMessages } from '../formatters/plain.js';
import { formatMessagesMarkdown } from '../formatters/markdown.js';
import { getOutputFormat } from '../formatters/index.js';
import chalk from 'chalk';
import ora from 'ora';
export const searchCommand = new Command('search')
.description('Search messages')
.argument('<query>', 'Search query')
.option('--chat <name>', 'Search within specific chat')
.option('--all', 'Search all chats (global search)')
.option('-n, --limit <number>', 'Maximum results', '50')
.option('--json', 'Output as JSON')
.option('--markdown', 'Output as Markdown')
.action(async (query, options) => {
const scope = options.chat ? `"${options.chat}"` : 'all chats';
const spinner = ora(`Searching for "${query}" in ${scope}...`).start();
try {
const client = await getClient();
const results = await searchMessages(client, query, {
chat: options.chat,
limit: parseInt(options.limit),
});
spinner.stop();
const format = getOutputFormat(options);
if (format === 'json') {
console.log(formatJson(results));
} else {
for (const result of results) {
if (result.messages.length === 0) {
console.log(chalk.yellow('No results found.'));
continue;
}
if (format === 'markdown') {
console.log(formatMessagesMarkdown(result.messages, result.chatTitle));
} else {
console.log(formatMessages(result.messages, result.chatTitle));
}
}
}
await disconnectClient();
} catch (error) {
spinner.fail('Search failed');
console.error(error instanceof Error ? error.message : error);
process.exit(1);
}
});
import { Command } from 'commander';
import { getClient, sendMessage, disconnectClient } from '../client.js';
import { formatJson } from '../formatters/json.js';
import { auditLog } from '../audit.js';
import { assertWriteEnabled } from '../guard.js';
import chalk from 'chalk';
import ora from 'ora';
export const sendCommand = new Command('send')
.description('Send a message')
.argument('<target>', 'Chat name, username (@user), or ID')
.argument('<message>', 'Message text')
.option('--json', 'Output as JSON')
.action(async (target, message, options) => {
assertWriteEnabled();
const spinner = ora(`Sending message to "${target}"...`).start();
try {
const client = await getClient();
const result = await sendMessage(client, target, message);
auditLog({ timestamp: new Date().toISOString(), command: 'send', target, message, result: { success: true, messageId: result.id } });
spinner.succeed(chalk.green('Message sent'));
if (options.json) {
console.log(formatJson({
id: result.id,
date: result.date ? new Date(result.date * 1000).toISOString() : null,
text: result.message,
}));
} else {
console.log(chalk.gray(`Message ID: ${result.id}`));
}
await disconnectClient();
} catch (error) {
auditLog({ timestamp: new Date().toISOString(), command: 'send', target, message, result: { success: false, error: error instanceof Error ? error.message : String(error) } });
spinner.fail('Failed to send message');
console.error(error instanceof Error ? error.message : error);
process.exit(1);
}
});
export * from './json.js';
export * from './plain.js';
export * from './markdown.js';
export type OutputFormat = 'plain' | 'json' | 'markdown';
export function getOutputFormat(options: { json?: boolean; markdown?: boolean; plain?: boolean }): OutputFormat {
if (options.json) return 'json';
if (options.markdown) return 'markdown';
return 'plain';
}
export function formatJson(data: unknown): string {
return JSON.stringify(data, null, 2);
}
Related skills
FAQ
What Telegram features does the telegram skill cover?
The telegram skill covers Bot API webhooks, command handlers, inline keyboards, and channel notifications. It helps developers connect backend or agent services to Telegram for inbound commands and outbound alerts.
Can agents use telegram for notifications?
Yes. The telegram skill supports wiring agent or API backends to send channel notifications and process user commands through Telegram webhooks, giving teams a lightweight messaging control surface.