telegram-tools 3.2.0__tar.gz → 3.3.0__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.
Files changed (59) hide show
  1. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/.github/pull_request_template.md +1 -0
  2. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/.github/workflows/tests.yml +2 -0
  3. telegram_tools-3.3.0/AGENTS.md +15 -0
  4. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/CHANGELOG.md +9 -0
  5. telegram_tools-3.3.0/CLAUDE.md +1 -0
  6. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/CONTRIBUTING.md +14 -0
  7. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/PKG-INFO +43 -11
  8. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/README.md +42 -10
  9. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/docs/telethon-api-notes.md +7 -0
  10. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/pyproject.toml +1 -1
  11. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/skill/SKILL.md +38 -9
  12. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/src/telegram_tools/__init__.py +1 -1
  13. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/src/telegram_tools/cli.py +122 -1
  14. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/src/telegram_tools/config.py +40 -0
  15. telegram_tools-3.3.0/src/telegram_tools/create.py +167 -0
  16. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/src/telegram_tools/doctor.py +13 -1
  17. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/src/telegram_tools/menu.py +143 -1
  18. telegram_tools-3.3.0/src/telegram_tools/send.py +116 -0
  19. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/tests/test_bot_cli.py +9 -0
  20. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/tests/test_cli.py +59 -0
  21. telegram_tools-3.3.0/tests/test_cli_resolution.py +252 -0
  22. telegram_tools-3.3.0/tests/test_create.py +142 -0
  23. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/tests/test_doctor.py +37 -0
  24. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/tests/test_menu.py +219 -29
  25. telegram_tools-3.3.0/tests/test_send.py +157 -0
  26. telegram_tools-3.2.0/tests/test_cli_resolution.py +0 -94
  27. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/.env.example +0 -0
  28. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  29. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  30. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  31. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/.gitignore +0 -0
  32. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/CODE_OF_CONDUCT.md +0 -0
  33. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/LICENSE +0 -0
  34. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/SECURITY.md +0 -0
  35. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/src/telegram_tools/bot_session.py +0 -0
  36. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/src/telegram_tools/bots.py +0 -0
  37. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/src/telegram_tools/client.py +0 -0
  38. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/src/telegram_tools/delete.py +0 -0
  39. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/src/telegram_tools/discovery.py +0 -0
  40. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/src/telegram_tools/exporters.py +0 -0
  41. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/src/telegram_tools/models.py +0 -0
  42. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/src/telegram_tools/prompts.py +0 -0
  43. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/src/telegram_tools/records.py +0 -0
  44. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/src/telegram_tools/resolver.py +0 -0
  45. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/src/telegram_tools/search.py +0 -0
  46. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/src/telegram_tools/topics.py +0 -0
  47. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/tests/test_bot_config.py +0 -0
  48. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/tests/test_bot_edits.py +0 -0
  49. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/tests/test_bot_session.py +0 -0
  50. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/tests/test_bots.py +0 -0
  51. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/tests/test_config.py +0 -0
  52. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/tests/test_delete.py +0 -0
  53. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/tests/test_discovery.py +0 -0
  54. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/tests/test_exporters.py +0 -0
  55. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/tests/test_prompts.py +0 -0
  56. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/tests/test_records.py +0 -0
  57. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/tests/test_resolver.py +0 -0
  58. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/tests/test_search.py +0 -0
  59. {telegram_tools-3.2.0 → telegram_tools-3.3.0}/tests/test_topics.py +0 -0
@@ -7,6 +7,7 @@
7
7
  - [ ] I did not include Telegram API hashes, bot tokens, phone numbers, `.env`, session files, or private exports.
8
8
  - [ ] I did not make destructive Telegram operations easier to run accidentally.
9
9
  - [ ] If I touched `clear-messages`, dry-run, `--execute`, typed `DELETE`, topic preservation, and topic ID preservation are covered by tests.
10
+ - [ ] If I touched `send`, the preview + `y/N` and the `TELEGRAM_SEND_ALLOWLIST` gate on `--yes` are covered by tests.
10
11
 
11
12
  ## Validation
12
13
 
@@ -46,5 +46,7 @@ jobs:
46
46
  telegram-tools discover --help
47
47
  telegram-tools clear-messages --help
48
48
  telegram-tools search --help
49
+ telegram-tools send --help
50
+ telegram-tools create --help
49
51
  telegram-tools bots --help
50
52
  telegram-tools doctor --help
@@ -0,0 +1,15 @@
1
+ # telegram-tools — agent brief
2
+
3
+ Local Telethon CLI operating the user's own Telegram account: `discover` chat/topic IDs, `search`/export messages, `send` a message, `create` group/channel/topic, `clear-messages`, `bots` (BotFather settings), `doctor`. Public repo — contributor-facing; never commit phone numbers, API hashes, tokens, or exported chat data (CONTRIBUTING.md binds).
4
+
5
+ ## Working here
6
+ - Test: `.venv/bin/python -m pytest -q` → all pass (300 as of v3.3.0), no network. CI adds `compileall` + per-subcommand `--help` smoke; there is no lint step.
7
+ - Bare invocation is the human menu; agents pass a subcommand. Session + exports live in `~/.telegram-tools/`, outside the repo.
8
+ - `skill/SKILL.md` (independently versioned) is the bundled agent skill's source of truth: a CLI-surface change updates it **in the same commit** — convention, nothing enforces it.
9
+ - A user-visible fix also gets its CHANGELOG entry and version bump in the same change; unreleased fixes silently stacking on the last shipped version is the failure mode to avoid.
10
+
11
+ ## Destructive commands
12
+ `clear-messages` deletes real messages: dry-run is the default, execution needs `--execute` plus typed `DELETE`. `bots` mutates live bot settings behind a diff + confirm (`--yes` skips). `send` posts publicly as the user behind a full-message preview + `y/N`; its `--yes` additionally requires the destination in `TELEGRAM_SEND_ALLOWLIST` (unset = every `--yes` send refused). `create` confirms before making a real, visible object. Keep those gates, and keep the menu from ever being the shorter path past one.
13
+
14
+ ## Releasing (maintainer only)
15
+ PyPI account is **banozz** (not the GitHub handle). Full recipe with the zsh traps (the `!` negation, the glob no-op, stale-sdist check, twine output redaction) lives in the maintainer's private runbook. Rebuild `dist/` after any source edit; `repo ≠ sdist` — `docs/superpowers/` is tracked here but excluded from the package.
@@ -4,6 +4,15 @@ All notable changes to this project will be documented here.
4
4
 
5
5
  This project follows a practical changelog style: user-visible changes, safety changes, and release notes belong here; active task tracking belongs outside the repo.
6
6
 
7
+ ## 3.3.0 - 2026-08-25
8
+
9
+ - Add `send`: post a message to a chat, or into one forum topic with `--topic`. It prints the destination and the whole message body, then asks `y/N`. `--text -` reads the body from stdin, which is how a multi-line message gets in without fighting your shell.
10
+ - Add `create`: `create group` (a supergroup, with `--forum` to switch topics on in the same call), `create channel` (a broadcast channel), and `create topic` (a topic inside a forum group). Each asks before it acts and prints the new ID, so a new chat can be piped straight into `send`.
11
+ - Both are in the menu, at 3 and 4. The other entries moved down: clearing topic messages is now 5, bots 6, check setup 7. The menu has no `--yes` equivalent — every send it makes shows the message and asks.
12
+ - New optional `TELEGRAM_SEND_ALLOWLIST` (`chat[:topic]`, comma separated). It gates one thing only: `send --yes`, the unattended path where nobody sees the preview. Unset means every `--yes` send is refused; `send` without `--yes` is unaffected. `doctor` reports how many destinations are listed and never which.
13
+ - The menu's staged message shows in the prompt when you go back to it (`Message [hiiiii]`), so keeping it does not mean typing it again. Long or multi-line bodies are flattened and cut for that one line only.
14
+ - No attachments on `send` — text only for now.
15
+
7
16
  ## 3.2.0 - 2026-08-15
8
17
 
9
18
  - `telegram-tools` with no arguments now opens a menu you walk forward and back through instead of a single screen that exits after one action: numbered lists all the way down, `0` always backs out one screen at a time — inside a picker or a flow's own screen alike — and a return to the menu after every job.
@@ -0,0 +1 @@
1
+ AGENTS.md
@@ -35,6 +35,8 @@ telegram-tools --help
35
35
  telegram-tools discover --help
36
36
  telegram-tools clear-messages --help
37
37
  telegram-tools search --help
38
+ telegram-tools send --help
39
+ telegram-tools create --help
38
40
  telegram-tools bots --help
39
41
  telegram-tools doctor --help
40
42
  ```
@@ -45,6 +47,18 @@ If you have local Telegram config and session storage, also run:
45
47
  telegram-tools doctor
46
48
  ```
47
49
 
50
+ ## Send Changes
51
+
52
+ `send` posts publicly as the person running it, so its gates are load-bearing:
53
+
54
+ - Without `--yes`, the whole message and its resolved destination are printed and a
55
+ `y/N` is required.
56
+ - `--yes` skips that prompt and therefore requires the destination to appear in
57
+ `TELEGRAM_SEND_ALLOWLIST`. An unset allowlist refuses every `--yes` send.
58
+ - The menu never passes `--yes`.
59
+
60
+ Keep all three covered by tests.
61
+
48
62
  ## Clear-Message Changes
49
63
 
50
64
  Be especially conservative around `clear-messages`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: telegram-tools
3
- Version: 3.2.0
3
+ Version: 3.3.0
4
4
  Summary: Local Telethon CLI for Telegram chat/topic ID discovery, message search/export, topic message clearing, and bot settings editing.
5
5
  Project-URL: Homepage, https://github.com/banozz0/telegram-tools
6
6
  Project-URL: Repository, https://github.com/banozz0/telegram-tools
@@ -49,7 +49,7 @@ Description-Content-Type: text/markdown
49
49
 
50
50
  # telegram-tools
51
51
 
52
- A local CLI for operating your own Telegram chats: find the real IDs of your groups, channels, and forum topics, search and export messages, and clear all messages out of forum topics without destroying the topics themselves.
52
+ A local CLI for operating your own Telegram chats: find the real IDs of your groups, channels, and forum topics, search and export messages, send a message into a chat or topic, create groups/channels/topics, and clear all messages out of forum topics without destroying the topics themselves.
53
53
 
54
54
  Built on [Telethon](https://github.com/LonamiWebs/Telethon). Everything runs on your machine with your own Telegram API credentials — no server, no third party, nothing leaves your computer except the Telegram API calls you asked for.
55
55
 
@@ -58,14 +58,16 @@ Built on [Telethon](https://github.com/LonamiWebs/Telethon). Everything runs on
58
58
  - **`discover`** — lists your chats, channels, and forum groups with their exact numeric IDs and every forum topic ID. The fastest way to answer "what is this chat's `-100…` ID and what are its topic IDs?"
59
59
  - **`search`** — searches messages by text, sender, date range, or topic, and prints a table or exports JSON/CSV.
60
60
  - **`clear-messages`** — deletes all messages inside selected forum topic(s) while preserving the topics and their IDs. Dry-run by default; deleting requires both `--execute` *and* typing `DELETE` at a prompt.
61
+ - **`send`** — posts a message to a chat, or into one forum topic. Shows you the whole message and its destination, then asks `y/N`.
62
+ - **`create`** — makes a supergroup (optionally with topics already on), a broadcast channel, or a topic inside a forum group, and prints the new ID.
61
63
  - **`bots`** — lists the bots you own with their numeric IDs, and edits what @BotFather edits: display name, bio, description, commands, profile photo, and default admin rights.
62
64
  - **`doctor`** — checks your local setup without printing any secrets.
63
65
 
64
66
  ## What it doesn't do (on purpose)
65
67
 
66
- - No deleting or creating forum topics — topic IDs never change.
67
- - No media downloads.
68
- - No sending messages and no automation loops. The `bots` command edits bot *settings*; it never runs a bot.
68
+ - No deleting forum topics, and no renaming them — `clear-messages` leaves topic IDs untouched.
69
+ - No media downloads, and no attachments on `send` — text only.
70
+ - No automation loops. The `bots` command edits bot *settings*; it never runs a bot.
69
71
  - No changing a bot's `@username`, creating or deleting bots, or reading/revoking bot tokens — those stay with @BotFather.
70
72
  - No cloud anything — credentials and session files stay in `~/.telegram-tools/`.
71
73
 
@@ -113,6 +115,21 @@ TELEGRAM_BOT_TOKENS=mybot:12345:AAExampleToken,alerts:67890:BBExampleToken
113
115
 
114
116
  Nicknames are yours to choose and can be used as `--bot mybot`. The tool only ever reads this variable — it never writes a token anywhere, and never prints one.
115
117
 
118
+ ### Optional: the send allowlist
119
+
120
+ `send` asks you to confirm every message. `--yes` skips that prompt, and because a
121
+ skipped prompt means nobody saw where the message was going, it only works for
122
+ destinations you have named in advance:
123
+
124
+ ```bash
125
+ # in ~/.telegram-tools/.env
126
+ TELEGRAM_SEND_ALLOWLIST=-1001234567890:141,-1009876543210,@myalerts
127
+ ```
128
+
129
+ Each entry is a chat ID or `@username`, optionally `:topic-id` to allow just one topic
130
+ in it. Unset means every `--yes` send is refused — `send` without `--yes` still works
131
+ and still asks. `doctor` reports how many destinations are listed, never which.
132
+
116
133
  ## 30 seconds of usage
117
134
 
118
135
  ```bash
@@ -131,6 +148,16 @@ telegram-tools clear-messages --chat @mygroup --topic 141
131
148
  # Actually delete: needs --execute AND typing DELETE at the prompt
132
149
  telegram-tools clear-messages --chat @mygroup --topic 141 --execute
133
150
 
151
+ # Send a message into a topic (shows it, then asks y/N)
152
+ telegram-tools send --chat -1001234567890 --topic 141 --text "deploy is green"
153
+
154
+ # Multi-line body, straight from a file or a pipe
155
+ cat notes.txt | telegram-tools send --chat -1001234567890 --text -
156
+
157
+ # Make a group with topics already switched on, then a topic in it
158
+ telegram-tools create group --title "Agency" --forum
159
+ telegram-tools create topic --chat -1001234567890 --title "Deploys"
160
+
134
161
  # Which bots do I own, and what are their IDs?
135
162
  telegram-tools bots
136
163
 
@@ -170,9 +197,11 @@ telegram-tools
170
197
  --------------------------------------------
171
198
  1. Chats & topics (find IDs)
172
199
  2. Search / export messages
173
- 3. Clear topic messages
174
- 4. My bots
175
- 5. Check setup
200
+ 3. Send a message
201
+ 4. Create a group, channel, or topic
202
+ 5. Clear topic messages
203
+ 6. My bots
204
+ 7. Check setup
176
205
  0. Exit
177
206
  ```
178
207
 
@@ -183,14 +212,17 @@ you to type an ID. Bot fields show their current value and offer keep / change /
183
212
  clear. After every job it returns to the menu.
184
213
 
185
214
  The safety gates are the same as the flags', not looser: clearing topic messages
186
- dry-runs first and still asks you to type `DELETE`, and bot edits still print a diff
187
- and ask before writing. With no terminal attached it prints this help instead.
215
+ dry-runs first and still asks you to type `DELETE`, sending shows the whole message
216
+ and asks `y/N`, and bot edits still print a diff and ask before writing. The menu has
217
+ no equivalent of `--yes` at all. With no terminal attached it prints this help instead.
188
218
 
189
219
  ## Safety model
190
220
 
191
221
  | Command | Destructive? |
192
222
  | --- | --- |
193
223
  | `discover`, `search`, `doctor` | No — read-only |
224
+ | `create` | No — makes new things, changes nothing existing, after a `y/N` unless you pass `--yes` |
225
+ | `send` | Outward-facing — posts publicly as you, after showing the whole message and asking `y/N`. `--yes` skips the prompt only for destinations in `TELEGRAM_SEND_ALLOWLIST` |
194
226
  | `bots` | No — changes settings on bots you own, after a diff and a `y/N` unless you pass `--yes`; reversible if you still have the old values, but `--remove-photo` and `--clear-commands` discard data Telegram will not hand back |
195
227
  | `clear-messages` | Yes — but only with `--execute` **and** a typed `DELETE`, only messages, never topics |
196
228
 
@@ -200,7 +232,7 @@ and ask before writing. With no terminal attached it prints this help instead.
200
232
 
201
233
  ## Status
202
234
 
203
- Stable for its four jobs; used regularly by its author. This is a solo project whose code was written by AI agents under review — issues are welcome, fixes are best-effort, and there is no support promise.
235
+ Stable for its six jobs; used regularly by its author. This is a solo project whose code was written by AI agents under review — issues are welcome, fixes are best-effort, and there is no support promise.
204
236
 
205
237
  ## License
206
238
 
@@ -1,6 +1,6 @@
1
1
  # telegram-tools
2
2
 
3
- A local CLI for operating your own Telegram chats: find the real IDs of your groups, channels, and forum topics, search and export messages, and clear all messages out of forum topics without destroying the topics themselves.
3
+ A local CLI for operating your own Telegram chats: find the real IDs of your groups, channels, and forum topics, search and export messages, send a message into a chat or topic, create groups/channels/topics, and clear all messages out of forum topics without destroying the topics themselves.
4
4
 
5
5
  Built on [Telethon](https://github.com/LonamiWebs/Telethon). Everything runs on your machine with your own Telegram API credentials — no server, no third party, nothing leaves your computer except the Telegram API calls you asked for.
6
6
 
@@ -9,14 +9,16 @@ Built on [Telethon](https://github.com/LonamiWebs/Telethon). Everything runs on
9
9
  - **`discover`** — lists your chats, channels, and forum groups with their exact numeric IDs and every forum topic ID. The fastest way to answer "what is this chat's `-100…` ID and what are its topic IDs?"
10
10
  - **`search`** — searches messages by text, sender, date range, or topic, and prints a table or exports JSON/CSV.
11
11
  - **`clear-messages`** — deletes all messages inside selected forum topic(s) while preserving the topics and their IDs. Dry-run by default; deleting requires both `--execute` *and* typing `DELETE` at a prompt.
12
+ - **`send`** — posts a message to a chat, or into one forum topic. Shows you the whole message and its destination, then asks `y/N`.
13
+ - **`create`** — makes a supergroup (optionally with topics already on), a broadcast channel, or a topic inside a forum group, and prints the new ID.
12
14
  - **`bots`** — lists the bots you own with their numeric IDs, and edits what @BotFather edits: display name, bio, description, commands, profile photo, and default admin rights.
13
15
  - **`doctor`** — checks your local setup without printing any secrets.
14
16
 
15
17
  ## What it doesn't do (on purpose)
16
18
 
17
- - No deleting or creating forum topics — topic IDs never change.
18
- - No media downloads.
19
- - No sending messages and no automation loops. The `bots` command edits bot *settings*; it never runs a bot.
19
+ - No deleting forum topics, and no renaming them — `clear-messages` leaves topic IDs untouched.
20
+ - No media downloads, and no attachments on `send` — text only.
21
+ - No automation loops. The `bots` command edits bot *settings*; it never runs a bot.
20
22
  - No changing a bot's `@username`, creating or deleting bots, or reading/revoking bot tokens — those stay with @BotFather.
21
23
  - No cloud anything — credentials and session files stay in `~/.telegram-tools/`.
22
24
 
@@ -64,6 +66,21 @@ TELEGRAM_BOT_TOKENS=mybot:12345:AAExampleToken,alerts:67890:BBExampleToken
64
66
 
65
67
  Nicknames are yours to choose and can be used as `--bot mybot`. The tool only ever reads this variable — it never writes a token anywhere, and never prints one.
66
68
 
69
+ ### Optional: the send allowlist
70
+
71
+ `send` asks you to confirm every message. `--yes` skips that prompt, and because a
72
+ skipped prompt means nobody saw where the message was going, it only works for
73
+ destinations you have named in advance:
74
+
75
+ ```bash
76
+ # in ~/.telegram-tools/.env
77
+ TELEGRAM_SEND_ALLOWLIST=-1001234567890:141,-1009876543210,@myalerts
78
+ ```
79
+
80
+ Each entry is a chat ID or `@username`, optionally `:topic-id` to allow just one topic
81
+ in it. Unset means every `--yes` send is refused — `send` without `--yes` still works
82
+ and still asks. `doctor` reports how many destinations are listed, never which.
83
+
67
84
  ## 30 seconds of usage
68
85
 
69
86
  ```bash
@@ -82,6 +99,16 @@ telegram-tools clear-messages --chat @mygroup --topic 141
82
99
  # Actually delete: needs --execute AND typing DELETE at the prompt
83
100
  telegram-tools clear-messages --chat @mygroup --topic 141 --execute
84
101
 
102
+ # Send a message into a topic (shows it, then asks y/N)
103
+ telegram-tools send --chat -1001234567890 --topic 141 --text "deploy is green"
104
+
105
+ # Multi-line body, straight from a file or a pipe
106
+ cat notes.txt | telegram-tools send --chat -1001234567890 --text -
107
+
108
+ # Make a group with topics already switched on, then a topic in it
109
+ telegram-tools create group --title "Agency" --forum
110
+ telegram-tools create topic --chat -1001234567890 --title "Deploys"
111
+
85
112
  # Which bots do I own, and what are their IDs?
86
113
  telegram-tools bots
87
114
 
@@ -121,9 +148,11 @@ telegram-tools
121
148
  --------------------------------------------
122
149
  1. Chats & topics (find IDs)
123
150
  2. Search / export messages
124
- 3. Clear topic messages
125
- 4. My bots
126
- 5. Check setup
151
+ 3. Send a message
152
+ 4. Create a group, channel, or topic
153
+ 5. Clear topic messages
154
+ 6. My bots
155
+ 7. Check setup
127
156
  0. Exit
128
157
  ```
129
158
 
@@ -134,14 +163,17 @@ you to type an ID. Bot fields show their current value and offer keep / change /
134
163
  clear. After every job it returns to the menu.
135
164
 
136
165
  The safety gates are the same as the flags', not looser: clearing topic messages
137
- dry-runs first and still asks you to type `DELETE`, and bot edits still print a diff
138
- and ask before writing. With no terminal attached it prints this help instead.
166
+ dry-runs first and still asks you to type `DELETE`, sending shows the whole message
167
+ and asks `y/N`, and bot edits still print a diff and ask before writing. The menu has
168
+ no equivalent of `--yes` at all. With no terminal attached it prints this help instead.
139
169
 
140
170
  ## Safety model
141
171
 
142
172
  | Command | Destructive? |
143
173
  | --- | --- |
144
174
  | `discover`, `search`, `doctor` | No — read-only |
175
+ | `create` | No — makes new things, changes nothing existing, after a `y/N` unless you pass `--yes` |
176
+ | `send` | Outward-facing — posts publicly as you, after showing the whole message and asking `y/N`. `--yes` skips the prompt only for destinations in `TELEGRAM_SEND_ALLOWLIST` |
145
177
  | `bots` | No — changes settings on bots you own, after a diff and a `y/N` unless you pass `--yes`; reversible if you still have the old values, but `--remove-photo` and `--clear-commands` discard data Telegram will not hand back |
146
178
  | `clear-messages` | Yes — but only with `--execute` **and** a typed `DELETE`, only messages, never topics |
147
179
 
@@ -151,7 +183,7 @@ and ask before writing. With no terminal attached it prints this help instead.
151
183
 
152
184
  ## Status
153
185
 
154
- Stable for its four jobs; used regularly by its author. This is a solo project whose code was written by AI agents under review — issues are welcome, fixes are best-effort, and there is no support promise.
186
+ Stable for its six jobs; used regularly by its author. This is a solo project whose code was written by AI agents under review — issues are welcome, fixes are best-effort, and there is no support promise.
155
187
 
156
188
  ## License
157
189
 
@@ -21,3 +21,10 @@ Checked on 2026-08-13 for the `bots` command (Telethon 1.44.0):
21
21
  - `photos.deletePhotos` needs an `InputPhoto` with an access hash, which `UserProfilePhoto` does not carry; fetch the photo through `photos.getUserPhotos` and convert it with `telethon.utils.get_input_photo`.
22
22
  - `bots.exportBotToken` exists and would let an owner read a bot's token. This project never calls it: it sits next to a `revoke` flag, and reading credentials is outside what the tool does.
23
23
  - Changing a bot's `@username`, creating a bot, and deleting a bot have no user-facing API and remain @BotFather-only.
24
+
25
+ Checked on 2026-08-25 for the `send` and `create` commands (Telethon 1.44.0):
26
+
27
+ - `TelegramClient.send_message(entity, message, reply_to=...)` posts into a forum topic by passing the topic id as `reply_to` — a topic *is* its root message thread, so there is no separate topic parameter.
28
+ - `channels.CreateChannelRequest` covers both a supergroup (`megagroup=True`) and a broadcast channel (`broadcast=True`), and takes a `forum` flag. Passing `forum=True` at creation avoids a second `channels.ToggleForumRequest` round trip, and with it the window where a group exists but the toggle failed.
29
+ - `messages.CreateForumTopicRequest` lives under `messages`, not `channels` (unlike `ToggleForumRequest`). Its `random_id` is auto-generated when omitted.
30
+ - Neither create request returns the created object directly. `CreateChannelRequest` returns `Updates` whose `chats[0]` is the new channel — `telethon.utils.get_peer_id` converts it to the marked `-100…` form. `CreateForumTopicRequest` returns `Updates` carrying only the topic's service message; that message's `id` **is** the new topic id.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "telegram-tools"
7
- version = "3.2.0"
7
+ version = "3.3.0"
8
8
  description = "Local Telethon CLI for Telegram chat/topic ID discovery, message search/export, topic message clearing, and bot settings editing."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: telegram-tools
3
- description: "Use when you need the real numeric ID of a Telegram chat, channel, group or forum topic — 'what's the ID of that topic?', 'which chat is -100…?', 'where do I send this?' — or when the user wants their own Telegram messages searched or exported to JSON/CSV."
4
- version: 1.1.0
3
+ description: "Use when you need the real numeric ID of a Telegram chat, channel, group or forum topic — 'what's the ID of that topic?', 'which chat is -100…?', 'where do I send this?' — when the user wants their own Telegram messages searched or exported to JSON/CSV, or when a message must be posted to a chat or topic the user has allowlisted."
4
+ version: 1.2.0
5
5
  author: banozz0
6
6
  license: MIT
7
7
  platforms: [macos]
8
8
  metadata:
9
9
  hermes:
10
- tags: [telegram, chat-ids, topic-ids, forum, search, export, cli]
10
+ tags: [telegram, chat-ids, topic-ids, forum, search, export, send, cli]
11
11
  ---
12
12
 
13
13
  # telegram-tools
@@ -40,8 +40,9 @@ their own message history searched, filtered or exported. It is the fastest way
40
40
  settle "which thread does this go to?", which is the single most common cause of a
41
41
  message being delivered into a topic nobody reads.
42
42
 
43
- It does not send messages, run bots, download media, or create and delete topics.
44
- Anything that *posts* to Telegram belongs to a bot token and a different tool.
43
+ It can also **send** a message and **create** a group, channel or topic. Those write
44
+ to Telegram as the user, so rules 2 and 3 below govern them — read those before
45
+ running either. It still does not run bots, download media, or delete topics.
45
46
 
46
47
  ## Hard rules
47
48
 
@@ -49,18 +50,31 @@ Anything that *posts* to Telegram belongs to a bot token and a different tool.
49
50
  it is their user account, the same one their friends message. Reads are read-only and
50
51
  fine. Anything that writes is theirs to run, not yours.
51
52
 
52
- **2. Never run `clear-messages`.** It deletes real messages out of their forum topics
53
+ **2. `send` only goes where the user already said it may.** `send --yes` posts with
54
+ no human in the loop, and the CLI refuses it for any destination not in the user's
55
+ `TELEGRAM_SEND_ALLOWLIST`. That refusal is the whole safety model — do not work
56
+ around it by dropping `--yes` (which would block on a `y/N` prompt no agent can
57
+ answer), by editing the user's `.env`, or by picking a different chat. A destination
58
+ that is not allowlisted is a destination the user has not approved: draft the message,
59
+ show it to them, and let them send it or add the entry.
60
+
61
+ **3. Never run `create` unprompted.** New groups, channels and topics are real,
62
+ visible objects in the user's Telegram — other people see them appear. Create one only
63
+ when the user asked for that specific thing in this conversation, and never invent a
64
+ title. `create` outside an explicit ask is theirs to run, not yours.
65
+
66
+ **4. Never run `clear-messages`.** It deletes real messages out of their forum topics
53
67
  and Telegram does not undo that. Dry-run is its default and the destructive path
54
68
  needs both `--execute` and a typed confirmation, so you will not trip it by accident
55
69
  — but do not run it at all, in any form, even to preview. If the answer is "those
56
70
  messages should go", say so and let the user run it.
57
71
 
58
- **3. Never print the credentials.** `TELEGRAM_API_ID`, `TELEGRAM_API_HASH` and the
72
+ **5. Never print the credentials.** `TELEGRAM_API_ID`, `TELEGRAM_API_HASH` and the
59
73
  `.session` file are secrets. Point at where they live; never read them out, copy
60
74
  them, or paste them into a reply. `doctor` exists precisely so setup can be checked
61
75
  without any of that reaching the screen.
62
76
 
63
- **4. If the CLI errors, say so.** A login prompt, a flood-wait, an expired session —
77
+ **6. If the CLI errors, say so.** A login prompt, a flood-wait, an expired session —
64
78
  that *is* the answer. Never guess a chat ID. A made-up `-100…` sends the user's next
65
79
  alert into the void, and they will not find out until something they needed never
66
80
  arrived.
@@ -76,6 +90,10 @@ arrived.
76
90
  | "find where X was discussed" | `telegram-tools search --chat <id> --keyword "X"` |
77
91
  | "everything in that topic since Monday" | `telegram-tools search --chat <id> --topic <topic-id> --since 2026-08-10` |
78
92
  | "export it" | `telegram-tools search --chat <id> --format csv --output /path/out.csv` |
93
+ | "post this to that topic" (allowlisted) | `telegram-tools send --chat <id> --topic <topic-id> --text "..." --yes` |
94
+ | a long or multi-line message | pipe it: `... \| telegram-tools send --chat <id> --text - --yes` |
95
+ | "make me a group with topics" (they asked) | `telegram-tools create group --title "..." --forum --yes` |
96
+ | "add a topic to that group" (they asked) | `telegram-tools create topic --chat <id> --title "..." --yes` |
79
97
  | "is telegram-tools set up?" | `telegram-tools doctor` |
80
98
 
81
99
  - **`discover` defaults to admin/managed chats only** — the ones the user runs. Add
@@ -87,6 +105,14 @@ arrived.
87
105
  Narrow with `--topic`, `--keyword`, `--from-user` (a username, an ID, or `me`),
88
106
  `--since` / `--until` (ISO dates), and `--limit`. With no `--output` it prints a
89
107
  readable table, which is usually what you want to summarise from.
108
+ - **`send` needs `--yes` from an agent session, and `--yes` needs the allowlist.**
109
+ Without `--yes` it prints the message and waits for a `y/N` nobody is there to
110
+ type. With `--yes` it refuses anything outside `TELEGRAM_SEND_ALLOWLIST` and the
111
+ error names the destination to add — relay that to the user verbatim rather than
112
+ retrying. `doctor` says how many destinations are listed, never which.
113
+ - **`send --topic` is the difference between delivered and lost.** Omitting it posts
114
+ to the chat itself, not the thread. Confirm the topic ID with `discover` first;
115
+ never guess one.
90
116
  - **Check the tool's own help before using a flag** that is not in this table. The
91
117
  CLI's `--help` is current; this file is a snapshot.
92
118
  - **The menu is for the human at the keyboard.** `telegram-tools` with no arguments
@@ -95,7 +121,10 @@ arrived.
95
121
 
96
122
  ## Never run these
97
123
 
98
- - **`clear-messages`** — irreversible deletion of the user's messages. Rule 2 above.
124
+ - **`clear-messages`** — irreversible deletion of the user's messages. Rule 4 above.
125
+ - **`create` on your own initiative** — rule 3. If a new group or topic looks like
126
+ the right answer, propose it and let the user say yes; do not create it and report
127
+ back.
99
128
  - **`bots`** — it edits a live bot's name, bio, description, commands, profile photo
100
129
  and default admin rights. Those are the user's public-facing bots; the edits are
101
130
  theirs to make. Check `--help` for whether the installed build has it at all.
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "3.2.0"
5
+ __version__ = "3.3.0"
@@ -4,6 +4,7 @@ import argparse
4
4
  import asyncio
5
5
  import json
6
6
  import sys
7
+ from functools import partial
7
8
  from pathlib import Path
8
9
  from typing import Sequence
9
10
 
@@ -24,12 +25,14 @@ from telegram_tools.bots import (
24
25
  )
25
26
  from telegram_tools.client import create_client
26
27
  from telegram_tools.config import ConfigError, bot_id_from_token, load_config, lookup_bot_token, resolve_bot_token
28
+ from telegram_tools.create import confirm_create, create_channel, create_group, create_topic, format_create_preview
27
29
  from telegram_tools.delete import confirm_clear_topic_messages, delete_topic_messages
28
30
  from telegram_tools.discovery import discover_chats, filter_chats, format_discovery_table
29
31
  from telegram_tools.doctor import run_doctor
30
32
  from telegram_tools.exporters import write_records
31
33
  from telegram_tools.resolver import EntityResolutionError, resolve_chat
32
34
  from telegram_tools.search import format_message_records, search_messages
35
+ from telegram_tools.send import SendTarget, confirm_send, format_send_preview, require_send_allowed, send_message
33
36
  from telegram_tools.topics import get_forum_topics, get_forum_topics_by_ids
34
37
 
35
38
 
@@ -85,6 +88,35 @@ def build_parser() -> argparse.ArgumentParser:
85
88
  bots_parser.add_argument("--channel-rights", help=f"Default admin rights for channels, comma-separated, or none (needs a bot token). Valid names: {valid_rights}")
86
89
  bots_parser.add_argument("--yes", action="store_true", help="Skip the confirmation prompt")
87
90
 
91
+ send_parser = subparsers.add_parser("send", help="Send a message to a chat or forum topic")
92
+ send_parser.add_argument("--chat", required=True, help="Chat/channel username, link, or ID")
93
+ send_parser.add_argument("--topic", type=int, help="Topic ID to post into; omit for the chat itself")
94
+ send_parser.add_argument("--text", required=True, help="Message text, or - to read it from stdin")
95
+ send_parser.add_argument(
96
+ "--yes",
97
+ action="store_true",
98
+ help="Skip the preview and send; the destination must be in TELEGRAM_SEND_ALLOWLIST",
99
+ )
100
+
101
+ create_parser = subparsers.add_parser("create", help="Create a group, channel, or forum topic")
102
+ create_kinds = create_parser.add_subparsers(dest="create_kind")
103
+
104
+ create_group_parser = create_kinds.add_parser("group", help="Create a supergroup, optionally with topics")
105
+ create_group_parser.add_argument("--title", required=True, help="Group name")
106
+ create_group_parser.add_argument("--about", help="Group description")
107
+ create_group_parser.add_argument("--forum", action="store_true", help="Enable topics on the new group")
108
+
109
+ create_channel_parser = create_kinds.add_parser("channel", help="Create a broadcast channel")
110
+ create_channel_parser.add_argument("--title", required=True, help="Channel name")
111
+ create_channel_parser.add_argument("--about", help="Channel description")
112
+
113
+ create_topic_parser = create_kinds.add_parser("topic", help="Create a topic in a forum group")
114
+ create_topic_parser.add_argument("--chat", required=True, help="Forum group username, link, or ID")
115
+ create_topic_parser.add_argument("--title", required=True, help="Topic name")
116
+
117
+ for kind_parser in (create_group_parser, create_channel_parser, create_topic_parser):
118
+ kind_parser.add_argument("--yes", action="store_true", help="Skip the confirmation prompt")
119
+
88
120
  subparsers.add_parser("doctor", help="Check local setup without printing secrets")
89
121
 
90
122
  return parser
@@ -154,6 +186,91 @@ async def _run_search(client, args) -> int:
154
186
  return 0
155
187
 
156
188
 
189
+ def _entity_title(entity, fallback: str) -> str:
190
+ """A chat's name for the preview: a title, a person's name, or what was typed."""
191
+ title = getattr(entity, "title", None)
192
+ if title:
193
+ return str(title)
194
+ parts = [getattr(entity, "first_name", None), getattr(entity, "last_name", None)]
195
+ name = " ".join(part for part in parts if part)
196
+ return name or str(getattr(entity, "username", None) or fallback)
197
+
198
+
199
+ def _message_text(raw: str) -> str:
200
+ # `-` is how a multi-line body gets in: quoting newlines through a shell flag is
201
+ # the kind of thing that silently sends half a message.
202
+ text = sys.stdin.read() if raw == "-" else raw
203
+ text = text.strip()
204
+ if not text:
205
+ raise ValueError("Nothing to send: the message text is empty.")
206
+ return text
207
+
208
+
209
+ async def _run_send(client, args, config) -> int:
210
+ text = _message_text(args.text)
211
+ resolved = await resolve_chat(client, args.chat)
212
+ peer = resolved.input_entity
213
+
214
+ topic = None
215
+ if args.topic is not None:
216
+ topics = await get_forum_topics_by_ids(client, peer, [args.topic])
217
+ topic = topics[0] if topics else None
218
+
219
+ target = SendTarget(chat_id=resolved.id, chat_title=_entity_title(resolved.entity, args.chat), topic=topic)
220
+
221
+ confirm = None
222
+ if args.yes:
223
+ require_send_allowed(
224
+ config.send_allowlist,
225
+ chat_id=resolved.id,
226
+ username=getattr(resolved.entity, "username", None),
227
+ topic_id=args.topic,
228
+ )
229
+ else:
230
+ preview = format_send_preview(target, text, sender=_entity_title(await client.get_me(), "you"))
231
+ confirm = partial(confirm_send, preview)
232
+
233
+ result = await send_message(client, peer, target, text, confirm=confirm)
234
+ print(json.dumps(result.to_dict(), indent=2))
235
+ return 1 if result.cancelled else 0
236
+
237
+
238
+ async def _run_create(client, args) -> int:
239
+ if args.create_kind is None:
240
+ raise ValueError("create needs one of: group, channel, topic.")
241
+
242
+ chat_title = None
243
+ peer = None
244
+ chat_id = None
245
+ if args.create_kind == "topic":
246
+ resolved = await resolve_chat(client, args.chat)
247
+ peer = resolved.input_entity
248
+ chat_id = resolved.id
249
+ chat_title = _entity_title(resolved.entity, args.chat)
250
+
251
+ forum = bool(getattr(args, "forum", False))
252
+ confirm = None
253
+ if not args.yes:
254
+ preview = format_create_preview(
255
+ args.create_kind,
256
+ args.title,
257
+ about=getattr(args, "about", None),
258
+ forum=forum,
259
+ chat_title=chat_title,
260
+ )
261
+ confirm = partial(confirm_create, preview)
262
+
263
+ if args.create_kind == "group":
264
+ created = await create_group(client, args.title, about=args.about, forum=forum, confirm=confirm)
265
+ elif args.create_kind == "channel":
266
+ created = await create_channel(client, args.title, about=args.about, confirm=confirm)
267
+ else:
268
+ created = await create_topic(client, peer, chat_id=chat_id, title=args.title, confirm=confirm)
269
+
270
+ print(json.dumps(created.to_dict(), indent=2))
271
+ return 1 if created.cancelled else 0
272
+
273
+
157
274
  EDIT_FLAGS = ("name", "bio", "description", "commands", "clear_commands", "photo", "remove_photo", "group_rights", "channel_rights")
158
275
 
159
276
 
@@ -224,7 +341,7 @@ async def _run_bots(client, args, config) -> int:
224
341
  return 0
225
342
 
226
343
  if not resolved.is_owned:
227
- raise PermissionError(f"You do not own @{profile.username or profile.id}; only its owner can edit it.")
344
+ raise PermissionError(f"You do not own {f'@{profile.username}' if profile.username else f'bot {profile.id}'}; only its owner can edit it.")
228
345
 
229
346
  if "commands" in requested:
230
347
  requested["commands"] = parse_commands_file(requested["commands"])
@@ -294,6 +411,10 @@ async def run(args, *, client=None, config=None) -> int:
294
411
  return await _run_search(client, args)
295
412
  if args.command == "bots":
296
413
  return await _run_bots(client, args, config)
414
+ if args.command == "send":
415
+ return await _run_send(client, args, config)
416
+ if args.command == "create":
417
+ return await _run_create(client, args)
297
418
  raise ValueError(f"Unknown command: {args.command}")
298
419
  finally:
299
420
  if owns_client: