@acedatacloud/skills 2026.621.1 → 2026.621.3
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 +1 -1
- package/skills/discordbot/SKILL.md +117 -0
- package/skills/telegram/SKILL.md +250 -125
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acedatacloud/skills",
|
|
3
|
-
"version": "2026.621.
|
|
3
|
+
"version": "2026.621.3",
|
|
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",
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: discordbot
|
|
3
|
+
description: List channels, read recent messages, and send messages on Discord using the user's own bot, via the Discord REST API. Use when the user wants their Discord BOT to post a message, read a channel, or list servers/channels — anything that acts in a server the bot was invited to.
|
|
4
|
+
when_to_use: |
|
|
5
|
+
Trigger when the user wants to send, read, or list things on Discord
|
|
6
|
+
through their bot: list the servers/channels the bot can see, read recent
|
|
7
|
+
messages in a channel, or post / reply in a channel. Messages are sent as
|
|
8
|
+
the BOT, not the user's personal account, and only in servers the bot has
|
|
9
|
+
been invited to with the right permissions.
|
|
10
|
+
connections: [discordbot]
|
|
11
|
+
allowed_tools: [Bash]
|
|
12
|
+
license: Apache-2.0
|
|
13
|
+
metadata:
|
|
14
|
+
author: acedatacloud
|
|
15
|
+
version: "1.0"
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
We drive the [Discord API](https://discord.com/developers/docs/reference)
|
|
19
|
+
with `curl + jq` using the user's **bot** token in `$DISCORDBOT_TOKEN`. The
|
|
20
|
+
auth header is `Authorization: Bot $DISCORDBOT_TOKEN` — note the literal
|
|
21
|
+
`Bot ` prefix (NOT `Bearer`). Base URL is `https://discord.com/api/v10`.
|
|
22
|
+
|
|
23
|
+
This acts as the user's registered **bot**, so it can only see and act in
|
|
24
|
+
servers (guilds) the bot has been **invited to** and only where it has the
|
|
25
|
+
relevant permission (View Channels / Send Messages / Read Message History).
|
|
26
|
+
A `403 Forbidden` (code 50001 "Missing Access" / 50013 "Missing
|
|
27
|
+
Permissions") almost always means the bot isn't in that server or lacks the
|
|
28
|
+
permission — tell the user to invite the bot or grant the permission rather
|
|
29
|
+
than retrying.
|
|
30
|
+
|
|
31
|
+
Errors are JSON `{"code": <n>, "message": "<reason>"}`. A `401` means the
|
|
32
|
+
bot token is wrong/reset — ask the user to re-paste it at
|
|
33
|
+
`auth.acedata.cloud/user/connections`. A `429` carries `retry_after`
|
|
34
|
+
(seconds) — sleep that long, then retry; never parallelize.
|
|
35
|
+
|
|
36
|
+
**Before sending a message, confirm the exact channel and content with the
|
|
37
|
+
user.** Sending is irreversible and public to that channel.
|
|
38
|
+
|
|
39
|
+
## Recipes
|
|
40
|
+
|
|
41
|
+
### Verify the bot (always run first)
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
curl -sS -H "Authorization: Bot $DISCORDBOT_TOKEN" \
|
|
45
|
+
"https://discord.com/api/v10/users/@me" \
|
|
46
|
+
| jq '{id, username, bot}'
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### List servers (guilds) the bot is in
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
curl -sS -H "Authorization: Bot $DISCORDBOT_TOKEN" \
|
|
53
|
+
"https://discord.com/api/v10/users/@me/guilds" \
|
|
54
|
+
| jq 'map({id, name})'
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### List text channels in a server
|
|
58
|
+
|
|
59
|
+
Channel `type` 0 = text, 5 = announcement; 2 = voice, 4 = category (skip
|
|
60
|
+
those for messaging). You need a guild id from the call above.
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
GUILD_ID="123456789012345678"
|
|
64
|
+
curl -sS -H "Authorization: Bot $DISCORDBOT_TOKEN" \
|
|
65
|
+
"https://discord.com/api/v10/guilds/$GUILD_ID/channels" \
|
|
66
|
+
| jq 'map(select(.type==0 or .type==5) | {id, name, type})'
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Read recent messages in a channel
|
|
70
|
+
|
|
71
|
+
Reading message **content** requires the **Message Content Intent** to be
|
|
72
|
+
enabled on the bot (Developer Portal → Bot → Privileged Gateway Intents).
|
|
73
|
+
Without it, `content` comes back empty for messages that don't mention the
|
|
74
|
+
bot. Needs the *Read Message History* permission in that channel.
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
CHANNEL_ID="123456789012345678"
|
|
78
|
+
curl -sS -H "Authorization: Bot $DISCORDBOT_TOKEN" \
|
|
79
|
+
"https://discord.com/api/v10/channels/$CHANNEL_ID/messages?limit=20" \
|
|
80
|
+
| jq 'map({author: .author.username, ts: .timestamp, content})'
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Send a message to a channel
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
CHANNEL_ID="123456789012345678"
|
|
87
|
+
curl -sS -X POST \
|
|
88
|
+
-H "Authorization: Bot $DISCORDBOT_TOKEN" \
|
|
89
|
+
-H "Content-Type: application/json" \
|
|
90
|
+
"https://discord.com/api/v10/channels/$CHANNEL_ID/messages" \
|
|
91
|
+
-d "$(jq -nc --arg c "Hello from the bot." '{content: $c}')"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Reply to a specific message
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
CHANNEL_ID="123456789012345678"; MESSAGE_ID="987654321098765432"
|
|
98
|
+
curl -sS -X POST \
|
|
99
|
+
-H "Authorization: Bot $DISCORDBOT_TOKEN" \
|
|
100
|
+
-H "Content-Type: application/json" \
|
|
101
|
+
"https://discord.com/api/v10/channels/$CHANNEL_ID/messages" \
|
|
102
|
+
-d "$(jq -nc --arg c "On it!" --arg m "$MESSAGE_ID" \
|
|
103
|
+
'{content: $c, message_reference: {message_id: $m}}')"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Notes
|
|
107
|
+
|
|
108
|
+
- A "server" in the UI is a "guild" in the API; messages live in channels
|
|
109
|
+
inside guilds. Always: list guilds → list that guild's channels → act on a
|
|
110
|
+
channel id. Don't invent ids.
|
|
111
|
+
- The bot only sees servers it was invited to. To add it: Developer Portal →
|
|
112
|
+
OAuth2 → URL Generator → scope `bot` + the permissions you need → open the
|
|
113
|
+
URL → pick a server (the user needs *Manage Server* there).
|
|
114
|
+
- This is a bot, not the user's account — it cannot read the user's DMs or
|
|
115
|
+
the user's full server list, only what the bot itself can access.
|
|
116
|
+
- Mentions: `<@USER_ID>` pings a user, `<#CHANNEL_ID>` links a channel. Plain
|
|
117
|
+
text is fine for normal messages.
|
package/skills/telegram/SKILL.md
CHANGED
|
@@ -1,118 +1,242 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: telegram
|
|
3
|
-
description:
|
|
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.
|
|
4
4
|
when_to_use: |
|
|
5
|
-
Trigger
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
Trigger for anything on the user's personal Telegram account: list recent
|
|
6
|
+
conversations or just the unread ones, read / summarize a chat or group,
|
|
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
|
|
10
|
+
the user's OWN account over MTProto (not a bot), so it sees everything they see.
|
|
10
11
|
connections: [telegram]
|
|
11
12
|
allowed_tools: [Bash]
|
|
12
13
|
license: Apache-2.0
|
|
13
14
|
metadata:
|
|
14
15
|
author: acedatacloud
|
|
15
|
-
version: "1.
|
|
16
|
+
version: "1.1"
|
|
16
17
|
---
|
|
17
18
|
|
|
18
|
-
We drive **personal** Telegram over
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
conversation, and message anyone the user can message.
|
|
19
|
+
We drive **personal** Telegram over MTProto with [Telethon](https://docs.telethon.dev/) —
|
|
20
|
+
this acts as the user's own account (a "userbot"), so unlike the Bot API it can read full
|
|
21
|
+
history, list every conversation, and act on anyone the user can reach.
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Credentials are injected as env vars by the connector:
|
|
24
24
|
|
|
25
|
-
- `TELEGRAM_API_ID` —
|
|
26
|
-
- `TELEGRAM_API_HASH` —
|
|
27
|
-
- `TELEGRAM_SESSION_STRING` —
|
|
25
|
+
- `TELEGRAM_API_ID` — app id
|
|
26
|
+
- `TELEGRAM_API_HASH` — app hash — **secret, never echo**
|
|
27
|
+
- `TELEGRAM_SESSION_STRING` — Telethon `StringSession` = **full account access. Never log,
|
|
28
28
|
echo, or print it.** Treat it like the account password.
|
|
29
29
|
|
|
30
30
|
## Setup — write the helper once per session
|
|
31
31
|
|
|
32
|
-
`telethon` is preinstalled in the sandbox
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
`telethon` is preinstalled in the sandbox. The helper is written to `./tg.py` **in the current
|
|
33
|
+
working directory** (the per-session workdir) — not a shared global path — so concurrent
|
|
34
|
+
sessions never race.
|
|
35
|
+
|
|
36
|
+
Every state-changing command (`send`, `reply`, `send-file`, `forward`, `edit`, `delete`,
|
|
37
|
+
`react`, `mark-read`) is **gated**: without a trailing `--confirm` it only DRY-RUNS (prints what
|
|
38
|
+
it would do, changes nothing). Read commands run directly. `--confirm` is honored **only as the
|
|
39
|
+
last argument** so a message/caption that merely contains "--confirm" can never silently confirm.
|
|
35
40
|
|
|
36
41
|
```sh
|
|
37
|
-
# telethon is preinstalled; the `|| pip install` is a best-effort fallback only
|
|
38
|
-
# (the sandbox is non-root, so a runtime install may not succeed — rely on the
|
|
39
|
-
# preinstalled package).
|
|
40
42
|
python3 -c "import telethon" 2>/dev/null || pip install --user --quiet telethon 2>/dev/null || true
|
|
41
43
|
|
|
42
44
|
cat > ./tg.py <<'PY'
|
|
43
45
|
import os, sys, json, asyncio
|
|
44
46
|
from telethon import TelegramClient
|
|
45
47
|
from telethon.sessions import StringSession
|
|
48
|
+
from telethon.tl import functions
|
|
49
|
+
from telethon.tl.types import ReactionEmoji
|
|
46
50
|
|
|
47
51
|
API_ID = int(os.environ["TELEGRAM_API_ID"])
|
|
48
52
|
API_HASH = os.environ["TELEGRAM_API_HASH"]
|
|
49
53
|
SESSION = os.environ["TELEGRAM_SESSION_STRING"]
|
|
50
54
|
|
|
55
|
+
_raw = sys.argv[1:]
|
|
56
|
+
# --confirm is only honored as the LAST token, and only one is stripped, so a
|
|
57
|
+
# message/caption that merely contains "--confirm" cannot silently confirm a write.
|
|
58
|
+
CONFIRM = bool(_raw) and _raw[-1] == "--confirm"
|
|
59
|
+
a = _raw[:-1] if CONFIRM else list(_raw)
|
|
60
|
+
cmd = a[0] if a else "help"
|
|
61
|
+
args = a[1:]
|
|
62
|
+
GATED = {"send", "reply", "send-file", "forward", "edit", "delete", "react", "mark-read"}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def out(o):
|
|
66
|
+
print(json.dumps(o, ensure_ascii=False, default=str))
|
|
67
|
+
|
|
51
68
|
|
|
52
69
|
async def resolve(client, target):
|
|
53
|
-
#
|
|
70
|
+
# 1) try direct resolve; 2) fall back to scanning dialogs by id or exact name
|
|
71
|
+
# (StringSession doesn't persist the entity cache, so a numeric id from a
|
|
72
|
+
# previous invocation may need the dialog scan to recover its access hash).
|
|
73
|
+
for attempt in (lambda: client.get_entity(int(target)), lambda: client.get_entity(target)):
|
|
74
|
+
try:
|
|
75
|
+
return await attempt()
|
|
76
|
+
except Exception:
|
|
77
|
+
pass
|
|
78
|
+
ti = None
|
|
54
79
|
try:
|
|
55
|
-
|
|
80
|
+
ti = int(target)
|
|
56
81
|
except (ValueError, TypeError):
|
|
57
82
|
pass
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if d.name == target:
|
|
63
|
-
return d.entity
|
|
64
|
-
raise ValueError(f"could not resolve target: {target}")
|
|
83
|
+
async for d in client.iter_dialogs():
|
|
84
|
+
if (ti is not None and d.id == ti) or d.name == target:
|
|
85
|
+
return d.entity
|
|
86
|
+
raise ValueError(f"could not resolve target: {target}")
|
|
65
87
|
|
|
66
88
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
89
|
+
def msg_row(m):
|
|
90
|
+
return {"id": m.id, "date": str(m.date), "out": m.out, "sender_id": m.sender_id,
|
|
91
|
+
"text": m.message, "media": bool(m.media)}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def need(n):
|
|
95
|
+
if len(args) < n:
|
|
96
|
+
raise ValueError(f"{cmd} needs {n} argument(s), got {len(args)}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def run():
|
|
100
|
+
if cmd in GATED and not CONFIRM:
|
|
101
|
+
out({"dry_run": True, "command": cmd, "args": args,
|
|
102
|
+
"note": "re-run with --confirm as the LAST argument to actually perform this write"})
|
|
103
|
+
return
|
|
104
|
+
async with TelegramClient(StringSession(SESSION), API_ID, API_HASH) as cl:
|
|
70
105
|
if cmd == "whoami":
|
|
71
|
-
me = await
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
106
|
+
me = await cl.get_me()
|
|
107
|
+
out({"id": me.id, "username": me.username,
|
|
108
|
+
"name": ((me.first_name or "") + " " + (me.last_name or "")).strip(), "phone": me.phone})
|
|
109
|
+
|
|
75
110
|
elif cmd == "list-chats":
|
|
76
|
-
limit = int(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
111
|
+
limit = int(args[0]) if args and args[0].lstrip("-").isdigit() else 20
|
|
112
|
+
unread_only = "unread-only" in args
|
|
113
|
+
res = []
|
|
114
|
+
async for d in cl.iter_dialogs(limit=limit):
|
|
115
|
+
if unread_only and not d.unread_count:
|
|
116
|
+
continue
|
|
117
|
+
res.append({"name": d.name, "id": d.id, "group": d.is_group,
|
|
80
118
|
"channel": d.is_channel, "user": d.is_user, "unread": d.unread_count})
|
|
81
|
-
|
|
119
|
+
out(res)
|
|
120
|
+
|
|
121
|
+
elif cmd == "unread":
|
|
122
|
+
res = []
|
|
123
|
+
async for d in cl.iter_dialogs():
|
|
124
|
+
if d.unread_count:
|
|
125
|
+
res.append({"name": d.name, "id": d.id, "unread": d.unread_count,
|
|
126
|
+
"group": d.is_group, "channel": d.is_channel})
|
|
127
|
+
out(sorted(res, key=lambda x: -x["unread"]))
|
|
128
|
+
|
|
82
129
|
elif cmd == "get-messages":
|
|
83
|
-
|
|
84
|
-
n = int(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
"sender_id": m.sender_id, "text": m.message})
|
|
90
|
-
out.reverse()
|
|
91
|
-
print(json.dumps(out, ensure_ascii=False))
|
|
130
|
+
need(1); ent = await resolve(cl, args[0])
|
|
131
|
+
n = int(args[1]) if len(args) > 1 else 50
|
|
132
|
+
rows = [msg_row(m) async for m in cl.iter_messages(ent, limit=n)]
|
|
133
|
+
rows.reverse()
|
|
134
|
+
out(rows)
|
|
135
|
+
|
|
92
136
|
elif cmd == "search":
|
|
93
|
-
|
|
94
|
-
n = int(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
137
|
+
need(2); ent = await resolve(cl, args[0])
|
|
138
|
+
q = args[1]; n = int(args[2]) if len(args) > 2 else 30
|
|
139
|
+
out([msg_row(m) async for m in cl.iter_messages(ent, search=q, limit=n)])
|
|
140
|
+
|
|
141
|
+
elif cmd == "search-global":
|
|
142
|
+
need(1); q = args[0]; n = int(args[1]) if len(args) > 1 else 30
|
|
143
|
+
rows = []
|
|
144
|
+
async for m in cl.iter_messages(None, search=q, limit=n):
|
|
145
|
+
r = msg_row(m); r["chat_id"] = m.chat_id
|
|
146
|
+
rows.append(r)
|
|
147
|
+
out(rows)
|
|
148
|
+
|
|
149
|
+
elif cmd == "contacts":
|
|
150
|
+
res = await cl(functions.contacts.GetContactsRequest(hash=0))
|
|
151
|
+
out([{"id": u.id, "username": u.username,
|
|
152
|
+
"name": ((u.first_name or "") + " " + (u.last_name or "")).strip(), "phone": u.phone}
|
|
153
|
+
for u in res.users])
|
|
154
|
+
|
|
155
|
+
elif cmd == "chat-info":
|
|
156
|
+
need(1); ent = await resolve(cl, args[0])
|
|
157
|
+
info = {"id": ent.id, "type": type(ent).__name__,
|
|
158
|
+
"title": getattr(ent, "title", None),
|
|
159
|
+
"name": ((getattr(ent, "first_name", "") or "") + " " + (getattr(ent, "last_name", "") or "")).strip() or None,
|
|
160
|
+
"username": getattr(ent, "username", None)}
|
|
161
|
+
try:
|
|
162
|
+
info["participants"] = (await cl.get_participants(ent, limit=1)).total
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
out(info)
|
|
166
|
+
|
|
167
|
+
elif cmd == "message-link":
|
|
168
|
+
need(2); ent = await resolve(cl, args[0]); mid = int(args[1])
|
|
169
|
+
try:
|
|
170
|
+
r = await cl(functions.channels.ExportMessageLinkRequest(channel=ent, id=mid))
|
|
171
|
+
out({"link": r.link})
|
|
172
|
+
except Exception as e:
|
|
173
|
+
out({"error": f"links only available for channels/supergroups: {e}"})
|
|
174
|
+
|
|
175
|
+
elif cmd == "download-media":
|
|
176
|
+
need(2); ent = await resolve(cl, args[0]); mid = int(args[1])
|
|
177
|
+
outdir = args[2] if len(args) > 2 else "./tg_downloads"
|
|
178
|
+
os.makedirs(outdir, exist_ok=True)
|
|
179
|
+
m = await cl.get_messages(ent, ids=mid)
|
|
180
|
+
if not m or not m.media:
|
|
181
|
+
out({"error": "no media on that message"}); return
|
|
182
|
+
path = await cl.download_media(m, file=outdir)
|
|
183
|
+
out({"downloaded": path})
|
|
184
|
+
|
|
185
|
+
# ---- gated writes (need trailing --confirm) ----
|
|
100
186
|
elif cmd == "send":
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
187
|
+
need(2); ent = await resolve(cl, args[0])
|
|
188
|
+
m = await cl.send_message(ent, args[1])
|
|
189
|
+
out({"sent": True, "id": m.id})
|
|
190
|
+
|
|
191
|
+
elif cmd == "reply":
|
|
192
|
+
need(3); ent = await resolve(cl, args[0])
|
|
193
|
+
m = await cl.send_message(ent, args[2], reply_to=int(args[1]))
|
|
194
|
+
out({"sent": True, "id": m.id, "reply_to": int(args[1])})
|
|
195
|
+
|
|
196
|
+
elif cmd == "send-file":
|
|
197
|
+
need(2); ent = await resolve(cl, args[0])
|
|
198
|
+
caption = args[2] if len(args) > 2 else None
|
|
199
|
+
m = await cl.send_file(ent, args[1], caption=caption)
|
|
200
|
+
out({"sent": True, "id": m.id})
|
|
201
|
+
|
|
202
|
+
elif cmd == "forward":
|
|
203
|
+
need(3); src = await resolve(cl, args[0]); mid = int(args[1]); dst = await resolve(cl, args[2])
|
|
204
|
+
fwd = await cl.forward_messages(dst, mid, src)
|
|
205
|
+
out({"forwarded": True, "id": getattr(fwd, "id", None) or [x.id for x in fwd]})
|
|
206
|
+
|
|
207
|
+
elif cmd == "edit":
|
|
208
|
+
need(3); ent = await resolve(cl, args[0])
|
|
209
|
+
m = await cl.edit_message(ent, int(args[1]), args[2])
|
|
210
|
+
out({"edited": True, "id": m.id})
|
|
211
|
+
|
|
212
|
+
elif cmd == "delete":
|
|
213
|
+
need(2); ent = await resolve(cl, args[0])
|
|
214
|
+
await cl.delete_messages(ent, int(args[1]))
|
|
215
|
+
out({"deleted": True, "id": int(args[1])})
|
|
216
|
+
|
|
217
|
+
elif cmd == "react":
|
|
218
|
+
need(3); ent = await resolve(cl, args[0]); mid = int(args[1]); emoji = args[2]
|
|
219
|
+
await cl(functions.messages.SendReactionRequest(
|
|
220
|
+
peer=ent, msg_id=mid, reaction=[ReactionEmoji(emoticon=emoji)]))
|
|
221
|
+
out({"reacted": True, "id": mid, "emoji": emoji})
|
|
222
|
+
|
|
223
|
+
elif cmd == "mark-read":
|
|
224
|
+
need(1); ent = await resolve(cl, args[0])
|
|
225
|
+
await cl.send_read_acknowledge(ent)
|
|
226
|
+
out({"marked_read": True})
|
|
227
|
+
|
|
113
228
|
else:
|
|
114
|
-
|
|
115
|
-
|
|
229
|
+
out({"error": f"unknown command: {cmd}"}); sys.exit(1)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
async def main():
|
|
233
|
+
try:
|
|
234
|
+
await run()
|
|
235
|
+
except SystemExit:
|
|
236
|
+
raise
|
|
237
|
+
except Exception as e:
|
|
238
|
+
out({"error": f"{type(e).__name__}: {e}"})
|
|
239
|
+
sys.exit(1)
|
|
116
240
|
|
|
117
241
|
|
|
118
242
|
asyncio.run(main())
|
|
@@ -120,76 +244,77 @@ PY
|
|
|
120
244
|
echo "helper ready"
|
|
121
245
|
```
|
|
122
246
|
|
|
123
|
-
## Verify the connection
|
|
247
|
+
## Verify the connection first
|
|
124
248
|
|
|
125
249
|
```sh
|
|
126
250
|
python3 ./tg.py whoami
|
|
127
|
-
# → {"id": 8367450178, "username": "GermeyAce", "name": "Germey"}
|
|
251
|
+
# → {"id": 8367450178, "username": "GermeyAce", "name": "Germey", "phone": "..."}
|
|
128
252
|
```
|
|
129
253
|
|
|
130
|
-
|
|
131
|
-
|
|
254
|
+
On an auth/session error the stored session is dead — tell the user to reconnect at
|
|
255
|
+
https://auth.acedata.cloud/user/connections.
|
|
132
256
|
|
|
133
|
-
##
|
|
257
|
+
## Read recipes
|
|
134
258
|
|
|
135
|
-
|
|
259
|
+
| Goal | Command |
|
|
260
|
+
|---|---|
|
|
261
|
+
| Recent conversations | `python3 ./tg.py list-chats 20` |
|
|
262
|
+
| Only chats with unread (ranked) | `python3 ./tg.py unread` |
|
|
263
|
+
| A chat's history (oldest→newest) | `python3 ./tg.py get-messages <target> 50` |
|
|
264
|
+
| Search inside one chat | `python3 ./tg.py search <target> "kw" 30` |
|
|
265
|
+
| Search across ALL chats | `python3 ./tg.py search-global "kw" 30` |
|
|
266
|
+
| List contacts | `python3 ./tg.py contacts` |
|
|
267
|
+
| Info about a chat/user | `python3 ./tg.py chat-info <target>` |
|
|
268
|
+
| t.me link to a message | `python3 ./tg.py message-link <target> <msg_id>` |
|
|
136
269
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
# → [{"name":"Ace <> ConduitOS","id":-5287630726,"group":true,...,"unread":0}, ...]
|
|
140
|
-
```
|
|
270
|
+
`<target>` = numeric id (most reliable — from `list-chats`), `@username`, phone, or exact chat
|
|
271
|
+
name. In message rows, `out:true` = sent by the user; `media:true` = has an attachment.
|
|
141
272
|
|
|
142
|
-
|
|
273
|
+
**Summarize-unread pattern**: `unread` → pick the chats that matter → `get-messages <id> N` on
|
|
274
|
+
each → summarize. Don't dump 20k messages; sample the most-unread / most-relevant.
|
|
143
275
|
|
|
144
|
-
|
|
276
|
+
## Media
|
|
145
277
|
|
|
146
278
|
```sh
|
|
147
|
-
#
|
|
148
|
-
python3 ./tg.py
|
|
149
|
-
|
|
279
|
+
# Download an attachment from a message → returns the saved path
|
|
280
|
+
python3 ./tg.py download-media <target> <msg_id> ./tg_downloads
|
|
281
|
+
# Send a local file or a URL (optional caption) — GATED
|
|
282
|
+
python3 ./tg.py send-file <target> /path/or/https-url "caption" --confirm
|
|
150
283
|
```
|
|
151
284
|
|
|
152
|
-
|
|
153
|
-
|
|
285
|
+
To hand a downloaded file back to the user as a link, upload it to the CDN (see the
|
|
286
|
+
`cos-upload` skill) after `download-media`.
|
|
154
287
|
|
|
155
|
-
|
|
288
|
+
## Write recipes — all GATED (dry-run unless trailing `--confirm`)
|
|
156
289
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
(Server-side search is scoped to one chat. To search broadly, list chats first, then search the
|
|
162
|
-
relevant ones.)
|
|
163
|
-
|
|
164
|
-
### Send / reply to a message — TWO-STEP, confirm first
|
|
165
|
-
|
|
166
|
-
Sending posts a **real message as the user**, so it is gated:
|
|
290
|
+
Sending/editing/deleting acts as the **real user**. Always run the dry run first, show the user
|
|
291
|
+
exactly what will happen, get an explicit "yes", then re-run with `--confirm` as the **last
|
|
292
|
+
argument**. Never bulk-send.
|
|
167
293
|
|
|
168
294
|
```sh
|
|
169
|
-
|
|
170
|
-
python3 ./tg.py
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
python3 ./tg.py
|
|
175
|
-
|
|
295
|
+
python3 ./tg.py send <target> "text" # → dry_run; add --confirm to send
|
|
296
|
+
python3 ./tg.py reply <target> <msg_id> "text" --confirm
|
|
297
|
+
python3 ./tg.py forward <from_target> <msg_id> <to_target> --confirm
|
|
298
|
+
python3 ./tg.py edit <target> <msg_id> "new text" --confirm # own messages
|
|
299
|
+
python3 ./tg.py delete <target> <msg_id> --confirm # destructive
|
|
300
|
+
python3 ./tg.py react <target> <msg_id> "👍" --confirm
|
|
301
|
+
python3 ./tg.py mark-read <target> --confirm # sends read receipts
|
|
176
302
|
```
|
|
177
303
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
-
|
|
185
|
-
|
|
186
|
-
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
- **
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
only on an exact match; usernames need the leading `@`.
|
|
304
|
+
The dry run returns `{"dry_run": true, "command": ..., "args": [...]}` — present that to the
|
|
305
|
+
user verbatim as the confirmation prompt.
|
|
306
|
+
|
|
307
|
+
## Gotchas — surface before the user is surprised
|
|
308
|
+
|
|
309
|
+
- **This is the user's real account.** Confirm before any write; reading exposes private chats.
|
|
310
|
+
- **`FloodWaitError`**: Telegram rate-limits userbots. On a flood-wait of N seconds, tell the
|
|
311
|
+
user to retry after N — never loop/retry aggressively (escalates toward a ban).
|
|
312
|
+
- **Dead session**: revoked from Telegram → Settings → Devices, or ~6-month inactivity. On
|
|
313
|
+
`AuthKeyError`/unauthorized, reconnect the connector (don't retry).
|
|
314
|
+
- **Never print `TELEGRAM_SESSION_STRING` / `TELEGRAM_API_HASH`** — full-account secrets.
|
|
315
|
+
- **Targets**: prefer the numeric `id` from `list-chats` (the helper recovers its access hash by
|
|
316
|
+
scanning dialogs); names need an exact match, usernames need a leading `@`.
|
|
317
|
+
- **`message-link`** only works for public channels/supergroups; private 1:1 / basic groups
|
|
318
|
+
return an error (no shareable link exists).
|
|
319
|
+
- **`edit`/`delete`** generally only apply to the user's own messages (admins can delete others
|
|
320
|
+
in groups they manage).
|