@acedatacloud/skills 2026.714.2 → 2026.714.4

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acedatacloud/skills",
3
- "version": "2026.714.2",
3
+ "version": "2026.714.4",
4
4
  "description": "Agent Skills for AceDataCloud AI services — music, image, video generation, LLM chat, web search. Compatible with Claude Code, GitHub Copilot, Gemini CLI, OpenAI Codex, and 30+ AI coding agents.",
5
5
  "keywords": [
6
6
  "agent-skills",
@@ -1,19 +1,20 @@
1
1
  ---
2
2
  name: telegram
3
- description: Full personal Telegram control over MTProto (Telethon) with the user's own account — list/search chats, read & summarize history, see unread, look up contacts & chat info, download media, and send / reply / forward / edit / delete / react / send files / mark read. Use when the user mentions Telegram, a Telegram chat/group/contact, "我的 Telegram", reading/replying/forwarding/summarizing Telegram messages, their unread Telegram, or sending a file/message on Telegram.
3
+ description: Full personal Telegram control over MTProto (Telethon) with the user's own account — list/search chats, read & summarize history, see unread, look up contacts & chat info, list a forum group's topics, download media, and send / reply / forward / edit / delete / react / send files / mark read / join & leave groups, including posting into a forum topic thread. Use when the user mentions Telegram, a Telegram chat/group/contact, "我的 Telegram", reading/replying/forwarding/summarizing Telegram messages, their unread Telegram, joining or leaving a Telegram group/channel, posting into a forum group's topic, or sending a file/message on Telegram.
4
4
  when_to_use: |
5
5
  Trigger for anything on the user's personal Telegram account: list recent
6
6
  conversations or just the unread ones, read / summarize a chat or group,
7
7
  search one chat or across all chats, look up a contact or a chat's info,
8
- download a photo/file from a message, or take an action — send, reply,
9
- forward, edit, delete, react, send a file, or mark a chat read. This drives
8
+ download a photo/file from a message, list a forum group's topics, or take an
9
+ action — send, reply, forward, edit, delete, react, send a file, mark a chat
10
+ read, post into a forum topic, or join / leave a group or channel. This drives
10
11
  the user's OWN account over MTProto (not a bot), so it sees everything they see.
11
12
  connections: [telegram]
12
13
  allowed_tools: [Bash]
13
14
  license: Apache-2.0
14
15
  metadata:
15
16
  author: acedatacloud
16
- version: "1.2"
17
+ version: "1.4"
17
18
  ---
18
19
 
19
20
  We drive **personal** Telegram over MTProto with [Telethon](https://docs.telethon.dev/) —
@@ -42,7 +43,7 @@ python3 "$TG" whoami
42
43
  ```
43
44
 
44
45
  Every state-changing command (`send`, `reply`, `send-file`, `forward`, `edit`, `delete`,
45
- `react`, `mark-read`) is **gated**: without a trailing `--confirm` it only DRY-RUNS (prints what
46
+ `react`, `mark-read`, `join`, `leave`) is **gated**: without a trailing `--confirm` it only DRY-RUNS (prints what
46
47
  it would do, changes nothing). Read commands run directly. `--confirm` is honored **only as the
47
48
  last argument** so a message/caption that merely contains "--confirm" can never silently confirm.
48
49
 
@@ -67,6 +68,7 @@ https://auth.acedata.cloud/user/connections.
67
68
  | Search across ALL chats | `python3 "$TG" search-global "kw" 30` |
68
69
  | List contacts | `python3 "$TG" contacts` |
69
70
  | Info about a chat/user | `python3 "$TG" chat-info <target>` |
71
+ | List a forum group's topics (threads) | `python3 "$TG" list-topics <target>` |
70
72
  | t.me link to a message | `python3 "$TG" message-link <target> <msg_id>` |
71
73
 
72
74
  `<target>` = numeric id (most reliable — from `list-chats`), `@username`, phone, or exact chat
@@ -100,14 +102,30 @@ argument**. Never bulk-send.
100
102
 
101
103
  ```sh
102
104
  python3 "$TG" send <target> "text" # → dry_run; add --confirm to send
105
+ python3 "$TG" send <target> "text" --topic <top_message> --confirm # into a forum topic thread
103
106
  python3 "$TG" reply <target> <msg_id> "text" --confirm
104
107
  python3 "$TG" forward <from_target> <msg_id> <to_target> --confirm
105
108
  python3 "$TG" edit <target> <msg_id> "new text" --confirm # own messages
106
109
  python3 "$TG" delete <target> <msg_id> --confirm # destructive
107
110
  python3 "$TG" react <target> <msg_id> "👍" --confirm
108
111
  python3 "$TG" mark-read <target> --confirm # sends read receipts
112
+ python3 "$TG" join <@username|t.me/link|t.me/+invite> --confirm # join a public group/channel or a private invite
113
+ python3 "$TG" leave <target> --confirm # leave a group/channel (not private chats)
109
114
  ```
110
115
 
116
+ `join` accepts a public `@username` / `t.me/<name>` (→ JoinChannelRequest) or a private
117
+ invite link `t.me/+HASH` / `t.me/joinchat/HASH` (→ ImportChatInviteRequest). Joining someone
118
+ else's group is a real membership change on the account — treat it like any other write:
119
+ dry-run, confirm, then `--confirm`. Never auto-join then bulk-message; that gets accounts
120
+ spam-limited.
121
+
122
+ **Posting into a forum group.** Some supergroups are *forums*: messages live in topics
123
+ (threads), and a plain `send` to the group root is rejected with `TOPIC_CLOSED`. Run
124
+ `list-topics <target>` first, pick a topic with `"closed": false` (an open chat/offtopic
125
+ thread — often titled `Беседка`/`Флуд`/`Chat`/`Offtopic`/`General`), then post with
126
+ `send <target> "text" --topic <top_message> --confirm` (its `top_message` id is the thread
127
+ anchor).
128
+
111
129
  The dry run returns `{"dry_run": true, "command": ..., "args": [...]}` — present that to the
112
130
  user verbatim as the confirmation prompt.
113
131
 
@@ -123,6 +141,11 @@ user verbatim as the confirmation prompt.
123
141
  scanning dialogs); names need an exact match, usernames need a leading `@`.
124
142
  - **`message-link`** only works for public channels/supergroups; private 1:1 / basic groups
125
143
  return an error (no shareable link exists).
144
+ - **`TOPIC_CLOSED` on send** = a forum supergroup; you can't post to the root. `list-topics`,
145
+ pick an open (`"closed": false`) thread, and `send ... --topic <top_message>`.
146
+ - **`ChatWriteForbiddenError` on send** = usually a channel's linked *discussion* group: you
147
+ must be subscribed to the parent channel (`join <parent_channel>`) before you can write in
148
+ its comment group, even though the group itself doesn't ban posting.
126
149
  - **`edit`/`delete`** generally only apply to the user's own messages (admins can delete others
127
150
  in groups they manage).
128
151
 
@@ -19,10 +19,10 @@ import tempfile
19
19
  import urllib.request
20
20
  from urllib.parse import urlparse
21
21
 
22
- from telethon import TelegramClient
22
+ from telethon import TelegramClient, errors, utils
23
23
  from telethon.sessions import StringSession
24
24
  from telethon.tl import functions
25
- from telethon.tl.types import ReactionEmoji
25
+ from telethon.tl.types import ReactionEmoji, User
26
26
 
27
27
  API_ID = int(os.environ["TELEGRAM_API_ID"])
28
28
  API_HASH = os.environ["TELEGRAM_API_HASH"]
@@ -35,7 +35,7 @@ CONFIRM = bool(_raw) and _raw[-1] == "--confirm"
35
35
  a = _raw[:-1] if CONFIRM else list(_raw)
36
36
  cmd = a[0] if a else "help"
37
37
  args = a[1:]
38
- GATED = {"send", "reply", "send-file", "forward", "edit", "delete", "react", "mark-read"}
38
+ GATED = {"send", "reply", "send-file", "forward", "edit", "delete", "react", "mark-read", "join", "leave"}
39
39
 
40
40
 
41
41
  def out(o):
@@ -117,6 +117,19 @@ def need(n):
117
117
  raise ValueError(f"{cmd} needs {n} argument(s), got {len(args)}")
118
118
 
119
119
 
120
+ def _pop_opt(argv, name):
121
+ """Extract `--name VALUE` from argv; return (value_or_None, remaining_argv).
122
+
123
+ Only a standalone `--name` token matches, so a message that merely contains
124
+ the flag text as part of one argument is left untouched.
125
+ """
126
+ if name in argv:
127
+ i = argv.index(name)
128
+ if i + 1 < len(argv):
129
+ return argv[i + 1], argv[:i] + argv[i + 2:]
130
+ return None, argv
131
+
132
+
120
133
  async def run():
121
134
  if cmd in GATED and not CONFIRM:
122
135
  out({"dry_run": True, "command": cmd, "args": args,
@@ -185,6 +198,18 @@ async def run():
185
198
  pass
186
199
  out(info)
187
200
 
201
+ elif cmd == "list-topics":
202
+ # Forum-style supergroups organize messages into topics (threads).
203
+ # A plain send to the group root is rejected (TOPIC_CLOSED); you must
204
+ # post into an open topic via `send --topic <top_message>`.
205
+ need(1); ent = await resolve(cl, args[0])
206
+ limit = int(args[1]) if len(args) > 1 else 100
207
+ res = await cl(functions.messages.GetForumTopicsRequest(
208
+ channel=ent, offset_date=0, offset_id=0, offset_topic=0, limit=limit))
209
+ out([{"id": getattr(t, "id", None), "title": getattr(t, "title", None),
210
+ "closed": getattr(t, "closed", None), "hidden": getattr(t, "hidden", None),
211
+ "top_message": getattr(t, "top_message", None)} for t in res.topics])
212
+
188
213
  elif cmd == "message-link":
189
214
  need(2); ent = await resolve(cl, args[0]); mid = int(args[1])
190
215
  try:
@@ -205,9 +230,15 @@ async def run():
205
230
 
206
231
  # ---- gated writes (need trailing --confirm) ----
207
232
  elif cmd == "send":
208
- need(2); ent = await resolve(cl, args[0])
209
- m = await cl.send_message(ent, args[1])
210
- out({"sent": True, "id": m.id})
233
+ # `--topic <top_message>` posts into a forum topic thread (reply_to);
234
+ # required for forum-style supergroups where a root send is rejected.
235
+ topic, sargs = _pop_opt(args, "--topic")
236
+ if len(sargs) < 2:
237
+ raise ValueError("send needs <target> <text> (optional --topic <id>)")
238
+ ent = await resolve(cl, sargs[0])
239
+ kw = {"reply_to": int(topic)} if topic is not None else {}
240
+ m = await cl.send_message(ent, sargs[1], **kw)
241
+ out({"sent": True, "id": m.id, "topic": int(topic) if topic is not None else None})
211
242
 
212
243
  elif cmd == "reply":
213
244
  need(3); ent = await resolve(cl, args[0])
@@ -252,6 +283,33 @@ async def run():
252
283
  await cl.send_read_acknowledge(ent)
253
284
  out({"marked_read": True})
254
285
 
286
+ elif cmd == "join":
287
+ need(1); target = args[0]
288
+ # A public @username / t.me/<user> resolves + JoinChannelRequest; a
289
+ # private invite (t.me/+HASH, joinchat/HASH, tg://join?invite=HASH)
290
+ # can't be resolved without joining, so import it by hash directly.
291
+ link_hash, is_invite = utils.parse_username(target)
292
+ try:
293
+ if is_invite:
294
+ res = await cl(functions.messages.ImportChatInviteRequest(link_hash))
295
+ chat = (getattr(res, "chats", None) or [None])[0]
296
+ out({"joined": True, "via": "invite",
297
+ "id": getattr(chat, "id", None), "title": getattr(chat, "title", None)})
298
+ else:
299
+ ent = await resolve(cl, target)
300
+ await cl(functions.channels.JoinChannelRequest(ent))
301
+ out({"joined": True, "via": "public", "id": ent.id,
302
+ "title": getattr(ent, "title", None), "username": getattr(ent, "username", None)})
303
+ except errors.UserAlreadyParticipantError:
304
+ out({"joined": True, "already_member": True, "target": target})
305
+
306
+ elif cmd == "leave":
307
+ need(1); ent = await resolve(cl, args[0])
308
+ if isinstance(ent, User):
309
+ out({"error": "leave applies to groups/channels, not private chats"}); return
310
+ await cl.delete_dialog(ent)
311
+ out({"left": True, "id": ent.id})
312
+
255
313
  else:
256
314
  out({"error": f"unknown command: {cmd}"}); sys.exit(1)
257
315