kctl-telegram 0.6.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kctl_telegram-0.6.3/.gitignore +33 -0
- kctl_telegram-0.6.3/PKG-INFO +17 -0
- kctl_telegram-0.6.3/README.md +71 -0
- kctl_telegram-0.6.3/pyproject.toml +45 -0
- kctl_telegram-0.6.3/skills/telegram-admin/SKILL.md +151 -0
- kctl_telegram-0.6.3/src/kctl_telegram/__init__.py +5 -0
- kctl_telegram-0.6.3/src/kctl_telegram/__main__.py +7 -0
- kctl_telegram-0.6.3/src/kctl_telegram/cli.py +131 -0
- kctl_telegram-0.6.3/src/kctl_telegram/commands/__init__.py +1 -0
- kctl_telegram-0.6.3/src/kctl_telegram/commands/bots.py +175 -0
- kctl_telegram-0.6.3/src/kctl_telegram/commands/chatwoot.py +105 -0
- kctl_telegram-0.6.3/src/kctl_telegram/commands/config_cmd.py +512 -0
- kctl_telegram-0.6.3/src/kctl_telegram/commands/dashboard.py +133 -0
- kctl_telegram-0.6.3/src/kctl_telegram/commands/doctor_cmd.py +57 -0
- kctl_telegram-0.6.3/src/kctl_telegram/commands/groups.py +125 -0
- kctl_telegram-0.6.3/src/kctl_telegram/commands/health.py +177 -0
- kctl_telegram-0.6.3/src/kctl_telegram/commands/messages.py +161 -0
- kctl_telegram-0.6.3/src/kctl_telegram/commands/skill_cmd.py +76 -0
- kctl_telegram-0.6.3/src/kctl_telegram/core/__init__.py +1 -0
- kctl_telegram-0.6.3/src/kctl_telegram/core/callbacks.py +33 -0
- kctl_telegram-0.6.3/src/kctl_telegram/core/client.py +76 -0
- kctl_telegram-0.6.3/src/kctl_telegram/core/config.py +119 -0
- kctl_telegram-0.6.3/src/kctl_telegram/core/exceptions.py +30 -0
- kctl_telegram-0.6.3/src/kctl_telegram/core/output.py +10 -0
- kctl_telegram-0.6.3/tests/conftest.py +60 -0
- kctl_telegram-0.6.3/tests/test_bots.py +244 -0
- kctl_telegram-0.6.3/tests/test_config.py +261 -0
- kctl_telegram-0.6.3/tests/test_groups.py +165 -0
- kctl_telegram-0.6.3/tests/test_health.py +139 -0
- kctl_telegram-0.6.3/tests/test_messages.py +289 -0
- kctl_telegram-0.6.3/tests/test_smoke.py +15 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
*.egg
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
.eggs/
|
|
9
|
+
|
|
10
|
+
# Virtual environments
|
|
11
|
+
.venv/
|
|
12
|
+
venv/
|
|
13
|
+
|
|
14
|
+
# IDE
|
|
15
|
+
.idea/
|
|
16
|
+
.vscode/
|
|
17
|
+
*.swp
|
|
18
|
+
*.swo
|
|
19
|
+
|
|
20
|
+
# Testing
|
|
21
|
+
.pytest_cache/
|
|
22
|
+
.coverage
|
|
23
|
+
htmlcov/
|
|
24
|
+
.mypy_cache/
|
|
25
|
+
.ruff_cache/
|
|
26
|
+
|
|
27
|
+
# OS
|
|
28
|
+
.DS_Store
|
|
29
|
+
Thumbs.db
|
|
30
|
+
|
|
31
|
+
# Environment
|
|
32
|
+
.env
|
|
33
|
+
.env.local
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kctl-telegram
|
|
3
|
+
Version: 0.6.3
|
|
4
|
+
Summary: Kodemeio Telegram CLI — bot platform management
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: httpx>=0.28.0
|
|
7
|
+
Requires-Dist: kctl-lib>=0.8.0
|
|
8
|
+
Requires-Dist: pydantic>=2.10.0
|
|
9
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
10
|
+
Requires-Dist: rich>=13.9.0
|
|
11
|
+
Requires-Dist: typer>=0.15.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: mypy>=1.14.0; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest-httpx>=0.35.0; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest>=8.3.0; extra == 'dev'
|
|
16
|
+
Requires-Dist: ruff>=0.9.0; extra == 'dev'
|
|
17
|
+
Requires-Dist: types-pyyaml>=6.0.0; extra == 'dev'
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# kctl-telegram
|
|
2
|
+
|
|
3
|
+
Kodemeio Telegram CLI — manage the multi-bot Telegram notification platform.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv tool install .
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
kctl-telegram config init
|
|
15
|
+
kctl-telegram health
|
|
16
|
+
kctl-telegram dashboard
|
|
17
|
+
kctl-telegram bots list
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Command Groups
|
|
21
|
+
|
|
22
|
+
| Group | Description |
|
|
23
|
+
|-------|-------------|
|
|
24
|
+
| `config` | Manage CLI configuration and profiles |
|
|
25
|
+
| `health` | Health checks and diagnostics |
|
|
26
|
+
| `dashboard` | System overview dashboard |
|
|
27
|
+
| `bots` | Manage Telegram bots |
|
|
28
|
+
| `groups` | Manage Telegram groups |
|
|
29
|
+
| `messages` | Send and manage messages |
|
|
30
|
+
| `chatwoot` | Manage Chatwoot integrations |
|
|
31
|
+
|
|
32
|
+
## Global Options
|
|
33
|
+
|
|
34
|
+
| Option | Short | Description |
|
|
35
|
+
|--------|-------|-------------|
|
|
36
|
+
| `--json` | | Output as JSON |
|
|
37
|
+
| `--quiet` | `-q` | Suppress info messages |
|
|
38
|
+
| `--format` | `-f` | Output format: pretty, json, csv, yaml |
|
|
39
|
+
| `--no-header` | | Suppress table headers (csv) |
|
|
40
|
+
| `--profile` | `-p` | Config profile name |
|
|
41
|
+
| `--url` | | API URL override |
|
|
42
|
+
| `--api-key` | | API key override |
|
|
43
|
+
| `--version` | `-V` | Show version and exit |
|
|
44
|
+
|
|
45
|
+
## Configuration
|
|
46
|
+
|
|
47
|
+
Config lives in `~/.config/kodemeio/config.yaml` under the `telegram` service key.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Initialize default profile
|
|
51
|
+
kctl-telegram config init
|
|
52
|
+
|
|
53
|
+
# Add a named profile
|
|
54
|
+
kctl-telegram config add prod \
|
|
55
|
+
--url https://telegram-api.example.com \
|
|
56
|
+
--api-key $TELEGRAM_API_KEY
|
|
57
|
+
|
|
58
|
+
# Switch active profile
|
|
59
|
+
kctl-telegram config use prod
|
|
60
|
+
|
|
61
|
+
# Show current profile (key masked)
|
|
62
|
+
kctl-telegram config show
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Development
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
uv run pytest tests/ -v
|
|
69
|
+
uv run ruff check src/
|
|
70
|
+
uv run mypy src/
|
|
71
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kctl-telegram"
|
|
7
|
+
version = "0.6.3"
|
|
8
|
+
description = "Kodemeio Telegram CLI — bot platform management"
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"kctl-lib>=0.8.0",
|
|
12
|
+
"typer>=0.15.0",
|
|
13
|
+
"rich>=13.9.0",
|
|
14
|
+
"pydantic>=2.10.0",
|
|
15
|
+
"pyyaml>=6.0.2",
|
|
16
|
+
"httpx>=0.28.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
dev = [
|
|
21
|
+
"pytest>=8.3.0",
|
|
22
|
+
"pytest-httpx>=0.35.0",
|
|
23
|
+
"ruff>=0.9.0",
|
|
24
|
+
"mypy>=1.14.0",
|
|
25
|
+
"types-PyYAML>=6.0.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
kctl-telegram = "kctl_telegram.cli:_run"
|
|
30
|
+
|
|
31
|
+
[tool.uv.sources]
|
|
32
|
+
kctl-lib = { workspace = true }
|
|
33
|
+
|
|
34
|
+
[project.entry-points."kctl_telegram.plugins"]
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.wheel]
|
|
37
|
+
packages = ["src/kctl_telegram"]
|
|
38
|
+
|
|
39
|
+
[tool.ruff]
|
|
40
|
+
target-version = "py312"
|
|
41
|
+
line-length = 120
|
|
42
|
+
|
|
43
|
+
[tool.mypy]
|
|
44
|
+
python_version = "3.12"
|
|
45
|
+
strict = true
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: telegram-admin
|
|
3
|
+
description: >
|
|
4
|
+
Kodemeio Telegram bot platform administration. Supports multiple instances
|
|
5
|
+
via profiles. Covers multi-bot management, group tracking, message sending/
|
|
6
|
+
broadcasting/scheduling, Chatwoot inbox integration, health monitoring,
|
|
7
|
+
and dashboard overview. Use when working with kctl-telegram CLI or managing
|
|
8
|
+
the kodemeio-telegram FastAPI service.
|
|
9
|
+
version: 1.0.0
|
|
10
|
+
allowed-tools:
|
|
11
|
+
- Bash
|
|
12
|
+
- Read
|
|
13
|
+
- Write
|
|
14
|
+
- Edit
|
|
15
|
+
- Glob
|
|
16
|
+
- Grep
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Telegram Administration
|
|
20
|
+
|
|
21
|
+
## Managed Instances
|
|
22
|
+
|
|
23
|
+
kctl-telegram supports multiple instances via profiles:
|
|
24
|
+
|
|
25
|
+
| Profile | URL | Use |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `kodemeio` | https://telegram.kodeme.io | Kodemeio notification bots |
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Target a specific instance
|
|
31
|
+
kctl-telegram -p kodemeio bots list
|
|
32
|
+
kctl-telegram -p kodemeio health
|
|
33
|
+
|
|
34
|
+
# Switch default profile
|
|
35
|
+
kctl-telegram config use kodemeio
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Config: `~/.config/kodemeio/config.yaml`
|
|
39
|
+
|
|
40
|
+
## CLI Tool: kctl-telegram
|
|
41
|
+
|
|
42
|
+
Installed globally via `uv tool install ./cli`. Run `kctl-telegram` from anywhere.
|
|
43
|
+
|
|
44
|
+
### Global Options
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
kctl-telegram [--json] [--quiet] [--profile NAME] [--url URL] [--api-key KEY] <command>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- `--profile / -p`: target a specific instance
|
|
51
|
+
- `--url`: override API base URL
|
|
52
|
+
- `--api-key`: override API key
|
|
53
|
+
- `--json`: output as JSON (for scripting/piping)
|
|
54
|
+
- `--quiet / -q`: suppress info messages
|
|
55
|
+
|
|
56
|
+
## Multi-Instance Management
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
kctl-telegram config init # Interactive setup
|
|
60
|
+
kctl-telegram config add <name> --url <url> [--api-key KEY] # Add/update profile
|
|
61
|
+
kctl-telegram config use <name> # Switch default
|
|
62
|
+
kctl-telegram config remove <name> [--service-only] [--force] # Remove profile
|
|
63
|
+
kctl-telegram config profiles # List all with status
|
|
64
|
+
kctl-telegram config current # Show active + connection
|
|
65
|
+
kctl-telegram config show # Full config (masked)
|
|
66
|
+
kctl-telegram config set <key> <value> # Edit config
|
|
67
|
+
kctl-telegram config test # Test connection
|
|
68
|
+
kctl-telegram config migrate # Migrate flat -> scoped format
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Health & Dashboard
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
kctl-telegram health [--watch] [--interval 10] # Health score (0-100)
|
|
75
|
+
kctl-telegram dashboard [--watch] [--interval 10] [--compact] # System overview
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Health scoring: API health (30pts) + readiness (30pts) + bots exist (20pts) + groups exist (20pts).
|
|
79
|
+
|
|
80
|
+
## Bot Management
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
kctl-telegram bots list # All registered bots
|
|
84
|
+
kctl-telegram bots get <id> # Bot details
|
|
85
|
+
kctl-telegram bots add --token <BOT_TOKEN> [--display-name NAME] # Register new bot
|
|
86
|
+
kctl-telegram bots update <id> [--display-name] [--is-active] # Update bot
|
|
87
|
+
kctl-telegram bots remove <id> [--force] # Deactivate bot
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Group Management
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
kctl-telegram groups list # All tracked groups
|
|
94
|
+
kctl-telegram groups get <id> # Group details
|
|
95
|
+
kctl-telegram groups update <id> --field <f> --value <v> # Update group settings
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Message Operations
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
kctl-telegram messages send --chat-id ID --text "msg" [--bot-id] [--parse-mode] # Send message
|
|
102
|
+
kctl-telegram messages broadcast --text "msg" [--bot-id] [--parse-mode] # Broadcast to all groups
|
|
103
|
+
kctl-telegram messages schedule --text "msg" --target-id ID --at "ISO" [--bot-id] # Schedule
|
|
104
|
+
kctl-telegram messages scheduled # List pending
|
|
105
|
+
kctl-telegram messages cancel <id> # Cancel scheduled
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Chatwoot Integration
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
kctl-telegram chatwoot list # List Chatwoot inboxes
|
|
112
|
+
kctl-telegram chatwoot add --bot-id ID --inbox-id ID --base-url URL --api-token TK # Add inbox
|
|
113
|
+
kctl-telegram chatwoot remove <id> [--force] # Remove inbox mapping
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## API Structure
|
|
117
|
+
|
|
118
|
+
The CLI talks to the kodemeio-telegram FastAPI service:
|
|
119
|
+
|
|
120
|
+
- **Base URL**: `https://telegram.kodeme.io/api/v1`
|
|
121
|
+
- **Auth**: `X-Api-Key` header
|
|
122
|
+
- **Resources**: bots, groups, messages, chatwoot/inboxes
|
|
123
|
+
- **Health**: GET /api/v1/health, GET /api/v1/ready
|
|
124
|
+
|
|
125
|
+
## Architecture
|
|
126
|
+
|
|
127
|
+
- **FastAPI** + python-telegram-bot (multi-bot, webhook mode)
|
|
128
|
+
- **PostgreSQL** (shared via kodemeio-postgres-16)
|
|
129
|
+
- **Redis** (rate limiting, caching)
|
|
130
|
+
- **BotManager** orchestrates multiple bots
|
|
131
|
+
- **APScheduler** for scheduled messages
|
|
132
|
+
- **Chatwoot/Odoo** integrations via webhook relay
|
|
133
|
+
|
|
134
|
+
## Troubleshooting
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# Check health
|
|
138
|
+
kctl-telegram health
|
|
139
|
+
|
|
140
|
+
# Watch health continuously
|
|
141
|
+
kctl-telegram health --watch
|
|
142
|
+
|
|
143
|
+
# Test connection
|
|
144
|
+
kctl-telegram config test
|
|
145
|
+
|
|
146
|
+
# Check bot status
|
|
147
|
+
kctl-telegram bots list
|
|
148
|
+
|
|
149
|
+
# JSON output for debugging
|
|
150
|
+
kctl-telegram --json bots list | jq .
|
|
151
|
+
```
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Main CLI entry point for kctl-telegram."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from kctl_telegram import __version__
|
|
11
|
+
from kctl_telegram.commands.bots import app as bots_app
|
|
12
|
+
from kctl_telegram.commands.chatwoot import app as chatwoot_app
|
|
13
|
+
from kctl_telegram.commands.config_cmd import app as config_app
|
|
14
|
+
from kctl_telegram.commands.dashboard import app as dashboard_app
|
|
15
|
+
from kctl_telegram.commands.groups import app as groups_app
|
|
16
|
+
from kctl_telegram.commands.health import app as health_app
|
|
17
|
+
from kctl_telegram.commands.messages import app as messages_app
|
|
18
|
+
from kctl_telegram.core.callbacks import AppContext
|
|
19
|
+
from kctl_telegram.core.exceptions import KctlError
|
|
20
|
+
from kctl_telegram.commands.skill_cmd import app as skill_app
|
|
21
|
+
from kctl_telegram.commands.doctor_cmd import app as doctor_app
|
|
22
|
+
from kctl_lib.self_update import notify_if_outdated
|
|
23
|
+
from kctl_lib.tui import add_tui_command
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def version_callback(value: bool) -> None:
|
|
27
|
+
if value:
|
|
28
|
+
typer.echo(f"kctl-telegram {__version__}")
|
|
29
|
+
raise typer.Exit()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
app = typer.Typer(
|
|
33
|
+
name="kctl-telegram",
|
|
34
|
+
help="Kodemeio Telegram CLI - manage your Telegram bot gateway.",
|
|
35
|
+
no_args_is_help=True,
|
|
36
|
+
rich_markup_mode="rich",
|
|
37
|
+
pretty_exceptions_enable=False,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.callback()
|
|
42
|
+
def main(
|
|
43
|
+
ctx: typer.Context,
|
|
44
|
+
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON")] = False,
|
|
45
|
+
quiet: Annotated[bool, typer.Option("--quiet", "-q", help="Suppress info messages")] = False,
|
|
46
|
+
format_: Annotated[str, typer.Option("--format", "-f", help="Output format: pretty/json/csv/yaml")] = "pretty",
|
|
47
|
+
no_header: Annotated[bool, typer.Option("--no-header", help="Suppress table headers (csv)")] = False,
|
|
48
|
+
profile: Annotated[str | None, typer.Option("--profile", "-p", help="Config profile name")] = None,
|
|
49
|
+
url: Annotated[str | None, typer.Option("--url", help="API URL override")] = None,
|
|
50
|
+
api_key: Annotated[str | None, typer.Option("--api-key", help="API key override")] = None,
|
|
51
|
+
version: Annotated[
|
|
52
|
+
bool, typer.Option("--version", "-V", callback=version_callback, is_eager=True, help="Show version")
|
|
53
|
+
] = False,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Kodemeio Telegram CLI."""
|
|
56
|
+
ctx.ensure_object(dict)
|
|
57
|
+
ctx.obj = AppContext(
|
|
58
|
+
json_mode=json_output,
|
|
59
|
+
quiet=quiet,
|
|
60
|
+
format=format_,
|
|
61
|
+
no_header=no_header,
|
|
62
|
+
profile=profile,
|
|
63
|
+
url_override=url,
|
|
64
|
+
api_key_override=api_key,
|
|
65
|
+
)
|
|
66
|
+
notify_if_outdated(ctx.obj.output, "kctl-telegram", __version__)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# Register all command groups
|
|
70
|
+
app.add_typer(config_app, name="config")
|
|
71
|
+
app.add_typer(health_app, name="health")
|
|
72
|
+
app.add_typer(dashboard_app, name="dashboard")
|
|
73
|
+
app.add_typer(bots_app, name="bots")
|
|
74
|
+
app.add_typer(groups_app, name="groups")
|
|
75
|
+
app.add_typer(messages_app, name="messages")
|
|
76
|
+
app.add_typer(chatwoot_app, name="chatwoot")
|
|
77
|
+
app.add_typer(skill_app, name="skill", hidden=True)
|
|
78
|
+
app.add_typer(doctor_app, name="doctor")
|
|
79
|
+
add_tui_command(app, service_key="telegram", version=__version__)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@app.command("self-update")
|
|
83
|
+
def self_update_cmd(ctx: typer.Context) -> None:
|
|
84
|
+
"""Check for updates and upgrade kctl-telegram."""
|
|
85
|
+
actx = ctx.obj
|
|
86
|
+
out = actx.output
|
|
87
|
+
|
|
88
|
+
from kctl_lib.self_update import check_update
|
|
89
|
+
from kctl_lib.self_update import update as do_update
|
|
90
|
+
|
|
91
|
+
latest = check_update("kctl-telegram", __version__)
|
|
92
|
+
if latest:
|
|
93
|
+
out.info(f"Updating to {latest}...")
|
|
94
|
+
do_update("kctl-telegram")
|
|
95
|
+
out.success(f"Updated to {latest}")
|
|
96
|
+
else:
|
|
97
|
+
out.success("Already up to date")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@app.command()
|
|
101
|
+
def completions(
|
|
102
|
+
shell: Annotated[str, typer.Argument(help="Shell type: zsh, bash, fish")] = "zsh",
|
|
103
|
+
install: Annotated[bool, typer.Option("--install", help="Install completions")] = False,
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Generate or install shell completions."""
|
|
106
|
+
from kctl_lib.completions import get_completion_script, install_completions
|
|
107
|
+
|
|
108
|
+
if install:
|
|
109
|
+
path = install_completions("kctl-telegram", shell)
|
|
110
|
+
if path:
|
|
111
|
+
typer.echo(f"Completions installed to {path}")
|
|
112
|
+
else:
|
|
113
|
+
typer.echo(f"Could not install completions for {shell}", err=True)
|
|
114
|
+
raise typer.Exit(code=1)
|
|
115
|
+
else:
|
|
116
|
+
script = get_completion_script("kctl-telegram", shell)
|
|
117
|
+
typer.echo(script)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _run() -> None:
|
|
121
|
+
"""Entry point with error handling."""
|
|
122
|
+
try:
|
|
123
|
+
app()
|
|
124
|
+
except KctlError as e:
|
|
125
|
+
from kctl_lib import handle_cli_error
|
|
126
|
+
|
|
127
|
+
handle_cli_error(e)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
if __name__ == "__main__":
|
|
131
|
+
_run()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Command modules for kctl-telegram."""
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Bot management commands.
|
|
2
|
+
|
|
3
|
+
List, inspect, create, update, and remove Telegram bots.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from kctl_telegram.core.callbacks import AppContext
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(help="Manage Telegram bots.")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command("list")
|
|
18
|
+
def list_(ctx: typer.Context) -> None:
|
|
19
|
+
"""List all registered bots."""
|
|
20
|
+
actx: AppContext = ctx.obj
|
|
21
|
+
out = actx.output
|
|
22
|
+
c = actx.client
|
|
23
|
+
|
|
24
|
+
data = c.get("bots")
|
|
25
|
+
bots = data.get("results", []) if isinstance(data, dict) else data if isinstance(data, list) else []
|
|
26
|
+
|
|
27
|
+
if not bots:
|
|
28
|
+
out.warn("No bots found.")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
rows: list[list[str]] = []
|
|
32
|
+
json_data: list[dict] = []
|
|
33
|
+
|
|
34
|
+
for bot in bots:
|
|
35
|
+
bot_id = str(bot.get("id", ""))
|
|
36
|
+
username = bot.get("username", "")
|
|
37
|
+
display_name = bot.get("display_name", "")
|
|
38
|
+
is_active = bot.get("is_active", False)
|
|
39
|
+
status = "[green]active[/green]" if is_active else "[red]inactive[/red]"
|
|
40
|
+
|
|
41
|
+
rows.append([bot_id, f"@{username}" if username else "-", display_name, status])
|
|
42
|
+
json_data.append(bot)
|
|
43
|
+
|
|
44
|
+
out.table(
|
|
45
|
+
"Bots",
|
|
46
|
+
[("ID", "cyan"), ("Username", ""), ("Display Name", ""), ("Status", "")],
|
|
47
|
+
rows,
|
|
48
|
+
data_for_json=json_data,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.command()
|
|
53
|
+
def get(
|
|
54
|
+
ctx: typer.Context,
|
|
55
|
+
bot_id: Annotated[int, typer.Argument(help="Bot ID")],
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Show bot details."""
|
|
58
|
+
actx: AppContext = ctx.obj
|
|
59
|
+
out = actx.output
|
|
60
|
+
c = actx.client
|
|
61
|
+
|
|
62
|
+
bot = c.get(f"bots/{bot_id}")
|
|
63
|
+
|
|
64
|
+
sections: list[tuple[str, list[tuple[str, str]]]] = []
|
|
65
|
+
|
|
66
|
+
info_kvs: list[tuple[str, str]] = [
|
|
67
|
+
("ID", str(bot.get("id", ""))),
|
|
68
|
+
("Username", f"@{bot.get('username', '')}" if bot.get("username") else "-"),
|
|
69
|
+
("Display Name", bot.get("display_name", "")),
|
|
70
|
+
("Is Active", "[green]Yes[/green]" if bot.get("is_active") else "[red]No[/red]"),
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
# Show token masked
|
|
74
|
+
token = bot.get("token", "")
|
|
75
|
+
if token:
|
|
76
|
+
masked = f"{token[:4]}{'*' * (len(token) - 8)}{token[-4:]}" if len(token) > 10 else "****"
|
|
77
|
+
info_kvs.append(("Token", masked))
|
|
78
|
+
|
|
79
|
+
sections.append(("Bot Info", info_kvs))
|
|
80
|
+
|
|
81
|
+
# Timestamps
|
|
82
|
+
ts_kvs: list[tuple[str, str]] = []
|
|
83
|
+
if bot.get("created_at"):
|
|
84
|
+
ts_kvs.append(("Created", bot["created_at"]))
|
|
85
|
+
if bot.get("updated_at"):
|
|
86
|
+
ts_kvs.append(("Updated", bot["updated_at"]))
|
|
87
|
+
if ts_kvs:
|
|
88
|
+
sections.append(("Timestamps", ts_kvs))
|
|
89
|
+
|
|
90
|
+
out.detail(f"Bot: {bot.get('display_name', bot.get('username', bot_id))}", sections, data_for_json=bot)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@app.command()
|
|
94
|
+
def add(
|
|
95
|
+
ctx: typer.Context,
|
|
96
|
+
token: Annotated[str, typer.Option("--token", help="Telegram bot token from BotFather.")],
|
|
97
|
+
display_name: Annotated[str | None, typer.Option("--display-name", help="Friendly display name.")] = None,
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Register a new Telegram bot."""
|
|
100
|
+
actx: AppContext = ctx.obj
|
|
101
|
+
out = actx.output
|
|
102
|
+
c = actx.client
|
|
103
|
+
|
|
104
|
+
payload: dict = {"token": token}
|
|
105
|
+
if display_name:
|
|
106
|
+
payload["display_name"] = display_name
|
|
107
|
+
|
|
108
|
+
bot = c.post("bots", json=payload)
|
|
109
|
+
|
|
110
|
+
out.success(f"Bot created: {bot.get('display_name', bot.get('username', ''))}")
|
|
111
|
+
out.kv("ID", str(bot.get("id", "")))
|
|
112
|
+
out.kv("Username", f"@{bot.get('username', '')}" if bot.get("username") else "-")
|
|
113
|
+
out.kv("Display Name", bot.get("display_name", ""))
|
|
114
|
+
out.kv("Active", str(bot.get("is_active", True)))
|
|
115
|
+
|
|
116
|
+
if actx.output.json_mode:
|
|
117
|
+
out.raw_json(bot)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@app.command()
|
|
121
|
+
def update(
|
|
122
|
+
ctx: typer.Context,
|
|
123
|
+
bot_id: Annotated[int, typer.Argument(help="Bot ID")],
|
|
124
|
+
display_name: Annotated[str | None, typer.Option("--display-name", help="New display name.")] = None,
|
|
125
|
+
is_active: Annotated[bool | None, typer.Option("--is-active/--no-active", help="Enable or disable bot.")] = None,
|
|
126
|
+
) -> None:
|
|
127
|
+
"""Update a bot's settings."""
|
|
128
|
+
actx: AppContext = ctx.obj
|
|
129
|
+
out = actx.output
|
|
130
|
+
c = actx.client
|
|
131
|
+
|
|
132
|
+
payload: dict = {}
|
|
133
|
+
if display_name is not None:
|
|
134
|
+
payload["display_name"] = display_name
|
|
135
|
+
if is_active is not None:
|
|
136
|
+
payload["is_active"] = is_active
|
|
137
|
+
|
|
138
|
+
if not payload:
|
|
139
|
+
out.warn("No changes specified. Use --display-name or --is-active/--no-active.")
|
|
140
|
+
raise typer.Exit(1)
|
|
141
|
+
|
|
142
|
+
bot = c.patch(f"bots/{bot_id}", json=payload)
|
|
143
|
+
|
|
144
|
+
out.success(f"Bot {bot_id} updated")
|
|
145
|
+
out.kv("Display Name", bot.get("display_name", ""))
|
|
146
|
+
out.kv("Active", str(bot.get("is_active", "")))
|
|
147
|
+
|
|
148
|
+
if actx.output.json_mode:
|
|
149
|
+
out.raw_json(bot)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@app.command()
|
|
153
|
+
def remove(
|
|
154
|
+
ctx: typer.Context,
|
|
155
|
+
bot_id: Annotated[int, typer.Argument(help="Bot ID")],
|
|
156
|
+
force: Annotated[bool, typer.Option("--force", help="Skip confirmation")] = False,
|
|
157
|
+
) -> None:
|
|
158
|
+
"""Remove a bot."""
|
|
159
|
+
actx: AppContext = ctx.obj
|
|
160
|
+
out = actx.output
|
|
161
|
+
c = actx.client
|
|
162
|
+
|
|
163
|
+
if not force:
|
|
164
|
+
# Fetch bot info for confirmation
|
|
165
|
+
try:
|
|
166
|
+
bot = c.get(f"bots/{bot_id}")
|
|
167
|
+
name = bot.get("display_name", bot.get("username", str(bot_id)))
|
|
168
|
+
except Exception:
|
|
169
|
+
name = str(bot_id)
|
|
170
|
+
|
|
171
|
+
if not typer.confirm(f"Remove bot '{name}' (ID: {bot_id})?"):
|
|
172
|
+
raise typer.Exit(0)
|
|
173
|
+
|
|
174
|
+
c.delete(f"bots/{bot_id}")
|
|
175
|
+
out.success(f"Bot {bot_id} removed")
|