clawgram 2.11.0 → 2.12.1
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/README.md +57 -0
- package/dist/channel.js +271 -1
- package/dist/gramjs-client.js +188 -0
- package/dist/history.js +1 -0
- package/dist/manage.js +287 -0
- package/dist/secret-refs.js +1 -1
- package/dist/silent-reaction.js +13 -2
- package/openclaw.plugin.json +53 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,6 +32,7 @@ Clawgram is a personal-Telegram channel plugin for [OpenClaw](https://github.com
|
|
|
32
32
|
- **Read receipts** — mark messages as read
|
|
33
33
|
- **Emoji reactions** — acknowledge a message with a reaction instead of a reply (`react` action)
|
|
34
34
|
- **Chat metadata** — title, type, member count, description, forum flag and pinned message (`chatInfo` action)
|
|
35
|
+
- **Chat management** — create supergroups, add/remove members, promote/demote admins, transfer ownership, export invite links (`createGroup`, `addMembers`, `removeMember`, `promoteAdmin`, `demoteAdmin`, `transferOwnership`, `inviteLink`) — off until `manageChats` allows it
|
|
35
36
|
- **User allowlist** — control which user has access to send messages for direct
|
|
36
37
|
- **Chat allowlist** — control which chats the assistant can access
|
|
37
38
|
- **Multi-account** — run multiple Telegram accounts simultaneously
|
|
@@ -226,6 +227,8 @@ openclaw gateway restart
|
|
|
226
227
|
| `allowFrom` | string[] | `["*"]` | Allowed sender IDs/usernames for direct messages only |
|
|
227
228
|
| `groups` | object | `{}` | Allowed groups map keyed by explicit group id or `*` |
|
|
228
229
|
| `proxy` | object | unset | Optional SOCKS4/SOCKS5 proxy for this account — see [Proxy (SOCKS4/SOCKS5)](#proxy-socks4socks5) |
|
|
230
|
+
| `manageChats` | string[] | unset | Chats the assistant may **manage** — see [Chat management](#chat-management). Absent or empty = management off; `["*"]` = every chat |
|
|
231
|
+
| `twoFaPassword` | string \| SecretRef | unset | The account's Telegram 2FA password; read only by `transferOwnership` |
|
|
229
232
|
|
|
230
233
|
Group config fields:
|
|
231
234
|
|
|
@@ -579,6 +582,58 @@ Bindings
|
|
|
579
582
|
```
|
|
580
583
|
|
|
581
584
|
|
|
585
|
+
## Chat management
|
|
586
|
+
|
|
587
|
+
Since 2.12.0 the assistant can assemble a chat, not only speak in it: create a supergroup, add and
|
|
588
|
+
remove members, promote and demote admins, hand the chat to a new owner, and issue invite links.
|
|
589
|
+
This exists because the plugin drives a personal MTProto account — Telegram's Bot API forbids most
|
|
590
|
+
of these to bots (a bot cannot create a group, cannot add an unwilling member, and cannot transfer
|
|
591
|
+
ownership at all).
|
|
592
|
+
|
|
593
|
+
**Off by default.** Every action below is gated by `accounts.*.manageChats`, and the default is the
|
|
594
|
+
exact opposite of `readChats`: absent or empty means *manage nothing*, a list of chat ids confines
|
|
595
|
+
management to those chats, `["*"]` allows every chat. A non-empty list also unlocks `createGroup`
|
|
596
|
+
(the chat being created is not in any list yet). All actions honour `dryRun`, and the gate is
|
|
597
|
+
checked before the dry-run answer, so a dry run exercises the same refusals a real call would hit.
|
|
598
|
+
|
|
599
|
+
| Action | Parameters | Notes |
|
|
600
|
+
|---|---|---|
|
|
601
|
+
| `createGroup` | `title`, `about?`, `users?` | Creates a **supergroup** (megagroup) — granular admin rights, bans and ownership transfer only exist there. Initial members are invited right after creation; who could not be added is returned in `missing` |
|
|
602
|
+
| `addMembers` | `chatId`, `users` | Adds to supergroups in one call, to basic groups one by one. Ids Telegram refused (privacy settings) come back in `missing` instead of failing the call |
|
|
603
|
+
| `removeMember` | `chatId`, `user`, `ban?` | Soft kick by default — the person may be re-invited later. `ban: true` keeps them out until unbanned |
|
|
604
|
+
| `promoteAdmin` | `chatId`, `user`, `rank?`, `rights?` | Grants a deliberate default set (change info, delete messages, ban, invite, pin, calls, topics). `addAdmins` and `anonymous` stay **off** unless explicitly set in `rights` |
|
|
605
|
+
| `demoteAdmin` | `chatId`, `user` | Strips every admin right |
|
|
606
|
+
| `transferOwnership` | `chatId`, `user` | Supergroups only. Requires `twoFaPassword` (below); Telegram's own rules surface as errors — see the fine print |
|
|
607
|
+
| `inviteLink` | `chatId`, `expireDate?`, `usageLimit?`, `title?`, `requestNeeded?` | The path for people whose privacy settings refuse a direct add. `expireDate` takes unix seconds or an ISO date |
|
|
608
|
+
|
|
609
|
+
User references in `users`/`user` are `@username` or a numeric Telegram id. A `@username` always
|
|
610
|
+
resolves; a bare numeric id only when the account has already seen the user (shared chat, dialog,
|
|
611
|
+
recent contact) — Telegram refuses to look up strangers by id.
|
|
612
|
+
|
|
613
|
+
**Ownership transfer fine print.** Telegram guards `transferOwnership` with an SRP proof of the
|
|
614
|
+
account's two-step-verification password, so the password has to be available to the runtime:
|
|
615
|
+
`accounts.*.twoFaPassword`, either a literal or a [SecretRef](#keeping-credentials-out-of-the-config-file).
|
|
616
|
+
It is read only by this action, exchanged for the SRP proof in-process, and never travels through
|
|
617
|
+
action parameters or logs. Telegram additionally refuses the transfer when 2FA was enabled less
|
|
618
|
+
than 7 days ago (`PASSWORD_TOO_FRESH_*`), when the session is younger than 24 hours
|
|
619
|
+
(`SESSION_TOO_FRESH_*`), or when the new owner is not yet an admin of the chat — promote them
|
|
620
|
+
first. These errors are surfaced as-is rather than retried.
|
|
621
|
+
|
|
622
|
+
```json
|
|
623
|
+
{
|
|
624
|
+
"channels": {
|
|
625
|
+
"clawgram": {
|
|
626
|
+
"accounts": {
|
|
627
|
+
"default": {
|
|
628
|
+
"manageChats": [ "-1001234567890" ],
|
|
629
|
+
"twoFaPassword": { "source": "file", "provider": "corp", "id": "/telegram/2fa-password" }
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
```
|
|
636
|
+
|
|
582
637
|
## Security and privacy
|
|
583
638
|
|
|
584
639
|
This plugin holds credentials for a real Telegram account and handles private correspondence. What
|
|
@@ -590,6 +645,8 @@ that means in practice, and what the code does about it:
|
|
|
590
645
|
| Proxy password | `accounts.*.proxy.password`, or a secret store | Also accepts a SecretRef since 2.2.0. Marked `sensitive` in `uiHints`; diagnostics say `socks4`/`socks5` and nothing more. An invalid proxy fails the account rather than falling back to a direct connection, which would leak the host IP to Telegram |
|
|
591
646
|
| Message bodies | channel logs | **Not logged.** Outbound sends record recipient, ids and `textLength`. Until 2.1.0 the full outbound text was written to the channel log — if you ran 2.0.x, treat those journal entries as containing private correspondence |
|
|
592
647
|
| Read scope | `accounts.*.readChats` | History and membership reads are confined to the listed chats. Absent means no restriction; an empty array denies everything |
|
|
648
|
+
| Manage scope | `accounts.*.manageChats` | Creating groups, changing membership, admin rights, ownership and invite links are confined to the listed chats — and **off entirely** when the key is absent or empty (opposite default to `readChats`, because these actions change chats rather than read them) |
|
|
649
|
+
| 2FA password | `accounts.*.twoFaPassword`, or a secret store | Read only by `transferOwnership`, exchanged for an SRP proof in-process. Accepts a SecretRef since 2.12.0; `sensitive` in `uiHints`; on the forbidden-log-keys list the static tests enforce |
|
|
593
650
|
| Who may talk to it | `allowFrom`, `groups.*.groupPolicy` | Direct-message senders and group behaviour are allowlisted; `mention` limits group replies to explicit mentions |
|
|
594
651
|
|
|
595
652
|
Two static tests (`test/no-secret-logging.test.ts`) fail the build if a message body or a credential
|
package/dist/channel.js
CHANGED
|
@@ -58,6 +58,7 @@ const normalize_1 = require("./normalize");
|
|
|
58
58
|
const history_1 = require("./history");
|
|
59
59
|
const joins_1 = require("./joins");
|
|
60
60
|
const reactions_1 = require("./reactions");
|
|
61
|
+
const manage_1 = require("./manage");
|
|
61
62
|
const silent_reaction_1 = require("./silent-reaction");
|
|
62
63
|
const chat_info_1 = require("./chat-info");
|
|
63
64
|
const secret_refs_1 = require("./secret-refs");
|
|
@@ -126,6 +127,48 @@ function readAccountReadChats(account) {
|
|
|
126
127
|
function resolveAccountReadChats(cfg, accountId) {
|
|
127
128
|
return readAccountReadChats(cfg?.channels?.["clawgram"]?.accounts?.[accountId]);
|
|
128
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Management scope as configured. Handed to `isChatManageable` raw: unlike
|
|
132
|
+
* `readChats`, an absent value already means "deny", so there is nothing to
|
|
133
|
+
* tell apart here.
|
|
134
|
+
*/
|
|
135
|
+
function resolveAccountManageChats(cfg, accountId) {
|
|
136
|
+
return cfg?.channels?.["clawgram"]?.accounts?.[accountId]?.manageChats;
|
|
137
|
+
}
|
|
138
|
+
/** Same normalization `readChats` gets, for the resolved-account copy. */
|
|
139
|
+
function readAccountManageChats(account) {
|
|
140
|
+
const raw = account?.manageChats;
|
|
141
|
+
if (raw === undefined || raw === null)
|
|
142
|
+
return undefined;
|
|
143
|
+
const entries = Array.isArray(raw) ? raw : [raw];
|
|
144
|
+
return entries.map((entry) => String(entry).trim()).filter(Boolean);
|
|
145
|
+
}
|
|
146
|
+
/** Canonical management action for every accepted spelling. */
|
|
147
|
+
const MANAGE_ACTION_ALIASES = {
|
|
148
|
+
createGroup: "createGroup",
|
|
149
|
+
createChat: "createGroup",
|
|
150
|
+
"create-group": "createGroup",
|
|
151
|
+
addMembers: "addMembers",
|
|
152
|
+
addMember: "addMembers",
|
|
153
|
+
"add-members": "addMembers",
|
|
154
|
+
removeMember: "removeMember",
|
|
155
|
+
removeMembers: "removeMember",
|
|
156
|
+
"remove-member": "removeMember",
|
|
157
|
+
kick: "removeMember",
|
|
158
|
+
promoteAdmin: "promoteAdmin",
|
|
159
|
+
promote: "promoteAdmin",
|
|
160
|
+
"promote-admin": "promoteAdmin",
|
|
161
|
+
setAdmin: "promoteAdmin",
|
|
162
|
+
demoteAdmin: "demoteAdmin",
|
|
163
|
+
demote: "demoteAdmin",
|
|
164
|
+
"demote-admin": "demoteAdmin",
|
|
165
|
+
transferOwnership: "transferOwnership",
|
|
166
|
+
transferOwner: "transferOwnership",
|
|
167
|
+
"transfer-ownership": "transferOwnership",
|
|
168
|
+
inviteLink: "inviteLink",
|
|
169
|
+
exportInviteLink: "inviteLink",
|
|
170
|
+
"invite-link": "inviteLink",
|
|
171
|
+
};
|
|
129
172
|
function parseOptionalThreadId(value) {
|
|
130
173
|
if (typeof value === "number") {
|
|
131
174
|
return Number.isFinite(value) ? Math.trunc(value) : undefined;
|
|
@@ -294,6 +337,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
294
337
|
"For Telegram forum topics, send to the group chat id and pass the topic id separately as `threadId`.",
|
|
295
338
|
"Use the `react` action to acknowledge a message with an emoji instead of sending a reply; pass an empty `emoji` (or `remove: true`) to take the reaction back.",
|
|
296
339
|
"Use the `chatInfo` action to learn what a chat is — title, type, member count, description, pinned message — instead of guessing from its id.",
|
|
340
|
+
"Use `createGroup` (title, optional about, optional users) to create a new Telegram supergroup; `addMembers`/`removeMember` change who is in a managed chat, `promoteAdmin`/`demoteAdmin` grant or revoke admin rights, `transferOwnership` hands the chat over, `inviteLink` issues an invite link for people Telegram refused to add directly.",
|
|
297
341
|
],
|
|
298
342
|
messageToolCapabilities: () => [
|
|
299
343
|
"clawgram can reply in the current Telegram conversation when no explicit target is provided.",
|
|
@@ -301,6 +345,7 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
301
345
|
"clawgram supports Telegram forum topics via the `threadId` parameter on group sends.",
|
|
302
346
|
"clawgram can add and clear emoji reactions on messages. A plain Telegram account holds one reaction per message, so a new emoji replaces the previous one.",
|
|
303
347
|
"clawgram can describe a chat via `chatInfo`: title, type (direct/group/supergroup/channel), member count, description, whether it is a forum, and the pinned message id.",
|
|
348
|
+
"clawgram can manage chats where the account's manageChats config allows it: create supergroups, add and remove members, promote and demote admins, transfer ownership, and export invite links.",
|
|
304
349
|
],
|
|
305
350
|
},
|
|
306
351
|
config: {
|
|
@@ -328,6 +373,11 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
328
373
|
// client from a config object this function had already stripped it
|
|
329
374
|
// from, so the setting validated, deployed and did nothing.
|
|
330
375
|
replyParseMode: account?.replyParseMode,
|
|
376
|
+
manageChats: readAccountManageChats(account),
|
|
377
|
+
// Optional secret: absent must stay absent, not become "".
|
|
378
|
+
twoFaPassword: account?.twoFaPassword === undefined || account?.twoFaPassword === null
|
|
379
|
+
? undefined
|
|
380
|
+
: (0, secret_refs_1.readSecretInput)(account.twoFaPassword),
|
|
331
381
|
};
|
|
332
382
|
},
|
|
333
383
|
},
|
|
@@ -1241,7 +1291,13 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
1241
1291
|
// Leaving it out does not degrade to a text send: the agent simply
|
|
1242
1292
|
// never sees a way to send the file, announces it in words, and the
|
|
1243
1293
|
// file stays on disk. That is exactly what happened on 2026-08-07.
|
|
1244
|
-
actions: [
|
|
1294
|
+
actions: [
|
|
1295
|
+
"send", "read", "participants", "joins", "react", "chatInfo", "upload-file",
|
|
1296
|
+
// Chat management (2.12.0) — gated by the account's manageChats
|
|
1297
|
+
// scope; without it every one of these is refused.
|
|
1298
|
+
"createGroup", "addMembers", "removeMember",
|
|
1299
|
+
"promoteAdmin", "demoteAdmin", "transferOwnership", "inviteLink",
|
|
1300
|
+
],
|
|
1245
1301
|
capabilities: [],
|
|
1246
1302
|
mediaSourceParams: {
|
|
1247
1303
|
"upload-file": ["filePath", "path", "media"],
|
|
@@ -1432,6 +1488,220 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
|
|
|
1432
1488
|
removed: reactionParams.remove,
|
|
1433
1489
|
});
|
|
1434
1490
|
}
|
|
1491
|
+
// ---- Chat management (2.12.0) ----
|
|
1492
|
+
//
|
|
1493
|
+
// Assembling a chat rather than speaking in it: create a supergroup,
|
|
1494
|
+
// add and remove people, appoint admins, hand the chat over, issue an
|
|
1495
|
+
// invite link. All of it is possible only because this is a personal
|
|
1496
|
+
// MTProto account — a bot could do almost none of this.
|
|
1497
|
+
//
|
|
1498
|
+
// Every branch is gated by the account's `manageChats` scope, which is
|
|
1499
|
+
// opt-in (absent = deny, see manage.ts) — these are the first actions
|
|
1500
|
+
// that change a chat rather than write into it. Parsing runs before
|
|
1501
|
+
// the gate so a malformed call fails on its own shape, and `dryRun`
|
|
1502
|
+
// returns after the gate so a dry run exercises the same refusals a
|
|
1503
|
+
// real call would hit. People's ids stay out of the logs throughout;
|
|
1504
|
+
// the JSON result carries them to the caller, the journal does not.
|
|
1505
|
+
const manageAction = MANAGE_ACTION_ALIASES[action];
|
|
1506
|
+
if (manageAction) {
|
|
1507
|
+
const manageAccountId = resolveRuntimeAccountId(cfg, accountId);
|
|
1508
|
+
if (!manageAccountId) {
|
|
1509
|
+
throw new Error("clawgram: no configured account found");
|
|
1510
|
+
}
|
|
1511
|
+
const manageScope = resolveAccountManageChats(cfg, manageAccountId);
|
|
1512
|
+
const requireManagedChat = (target) => {
|
|
1513
|
+
if (!(0, manage_1.isChatManageable)(target, manageScope)) {
|
|
1514
|
+
actionLog.warn("clawgram management refused: chat outside manage scope", {
|
|
1515
|
+
accountId: manageAccountId,
|
|
1516
|
+
action: manageAction,
|
|
1517
|
+
target,
|
|
1518
|
+
});
|
|
1519
|
+
throw new Error(`clawgram: not-managed-chat ${target}`);
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
const requireRuntime = () => {
|
|
1523
|
+
const gram = runtimes.get(manageAccountId);
|
|
1524
|
+
if (!gram) {
|
|
1525
|
+
throw new Error(`clawgram: runtime not found for account ${manageAccountId}`);
|
|
1526
|
+
}
|
|
1527
|
+
return gram;
|
|
1528
|
+
};
|
|
1529
|
+
if (manageAction === "createGroup") {
|
|
1530
|
+
const createParams = (0, manage_1.parseCreateGroupParams)(params);
|
|
1531
|
+
// A group being created is not in any scope yet, so the gate is
|
|
1532
|
+
// coarser: management must be enabled at all for this account.
|
|
1533
|
+
if (!(0, manage_1.isManagementEnabled)(manageScope)) {
|
|
1534
|
+
actionLog.warn("clawgram createGroup refused: management is not enabled", {
|
|
1535
|
+
accountId: manageAccountId,
|
|
1536
|
+
});
|
|
1537
|
+
throw new Error("clawgram: chat management is not enabled for this account — "
|
|
1538
|
+
+ `set channels.clawgram.accounts.${manageAccountId}.manageChats`);
|
|
1539
|
+
}
|
|
1540
|
+
actionLog.info("clawgram handleAction createGroup", {
|
|
1541
|
+
accountId: manageAccountId,
|
|
1542
|
+
dryRun: dryRun === true,
|
|
1543
|
+
users: createParams.users.length,
|
|
1544
|
+
hasAbout: Boolean(createParams.about),
|
|
1545
|
+
});
|
|
1546
|
+
if (dryRun === true) {
|
|
1547
|
+
return (0, core_1.jsonResult)({ ok: true, dryRun: true, accountId: manageAccountId });
|
|
1548
|
+
}
|
|
1549
|
+
const created = await requireRuntime().createGroup(createParams);
|
|
1550
|
+
actionLog.info("clawgram handleAction createGroup completed", {
|
|
1551
|
+
accountId: manageAccountId,
|
|
1552
|
+
chatId: created.chatId ?? null,
|
|
1553
|
+
missing: created.missing.length,
|
|
1554
|
+
});
|
|
1555
|
+
return (0, core_1.jsonResult)({
|
|
1556
|
+
ok: true,
|
|
1557
|
+
accountId: manageAccountId,
|
|
1558
|
+
chatId: created.chatId,
|
|
1559
|
+
missing: created.missing,
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
if (manageAction === "addMembers") {
|
|
1563
|
+
const addParams = (0, manage_1.parseAddMembersParams)(params, toolContext);
|
|
1564
|
+
requireManagedChat(addParams.target);
|
|
1565
|
+
actionLog.info("clawgram handleAction addMembers", {
|
|
1566
|
+
accountId: manageAccountId,
|
|
1567
|
+
dryRun: dryRun === true,
|
|
1568
|
+
target: addParams.target,
|
|
1569
|
+
users: addParams.users.length,
|
|
1570
|
+
});
|
|
1571
|
+
if (dryRun === true) {
|
|
1572
|
+
return (0, core_1.jsonResult)({ ok: true, dryRun: true, accountId: manageAccountId, chatId: addParams.target });
|
|
1573
|
+
}
|
|
1574
|
+
const added = await requireRuntime().addChatMembers(addParams);
|
|
1575
|
+
actionLog.info("clawgram handleAction addMembers completed", {
|
|
1576
|
+
accountId: manageAccountId,
|
|
1577
|
+
target: addParams.target,
|
|
1578
|
+
requested: addParams.users.length,
|
|
1579
|
+
missing: added.missing.length,
|
|
1580
|
+
});
|
|
1581
|
+
return (0, core_1.jsonResult)({
|
|
1582
|
+
ok: true,
|
|
1583
|
+
accountId: manageAccountId,
|
|
1584
|
+
chatId: added.chatId ?? addParams.target,
|
|
1585
|
+
requested: addParams.users.length,
|
|
1586
|
+
// Telegram refuses silently-restricted invites per user; the
|
|
1587
|
+
// caller gets the ids so it can hand them an invite link.
|
|
1588
|
+
missing: added.missing,
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
if (manageAction === "removeMember") {
|
|
1592
|
+
const removeParams = (0, manage_1.parseRemoveMemberParams)(params, toolContext);
|
|
1593
|
+
requireManagedChat(removeParams.target);
|
|
1594
|
+
actionLog.info("clawgram handleAction removeMember", {
|
|
1595
|
+
accountId: manageAccountId,
|
|
1596
|
+
dryRun: dryRun === true,
|
|
1597
|
+
target: removeParams.target,
|
|
1598
|
+
ban: removeParams.ban,
|
|
1599
|
+
});
|
|
1600
|
+
if (dryRun === true) {
|
|
1601
|
+
return (0, core_1.jsonResult)({ ok: true, dryRun: true, accountId: manageAccountId, chatId: removeParams.target });
|
|
1602
|
+
}
|
|
1603
|
+
await requireRuntime().removeChatMember(removeParams);
|
|
1604
|
+
actionLog.info("clawgram handleAction removeMember completed", {
|
|
1605
|
+
accountId: manageAccountId,
|
|
1606
|
+
target: removeParams.target,
|
|
1607
|
+
ban: removeParams.ban,
|
|
1608
|
+
});
|
|
1609
|
+
return (0, core_1.jsonResult)({
|
|
1610
|
+
ok: true,
|
|
1611
|
+
accountId: manageAccountId,
|
|
1612
|
+
chatId: removeParams.target,
|
|
1613
|
+
user: removeParams.user,
|
|
1614
|
+
banned: removeParams.ban,
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
if (manageAction === "promoteAdmin" || manageAction === "demoteAdmin") {
|
|
1618
|
+
const adminParams = manageAction === "promoteAdmin"
|
|
1619
|
+
? (0, manage_1.parsePromoteAdminParams)(params, toolContext)
|
|
1620
|
+
: (0, manage_1.parseDemoteAdminParams)(params, toolContext);
|
|
1621
|
+
requireManagedChat(adminParams.target);
|
|
1622
|
+
actionLog.info("clawgram handleAction setAdmin", {
|
|
1623
|
+
accountId: manageAccountId,
|
|
1624
|
+
dryRun: dryRun === true,
|
|
1625
|
+
target: adminParams.target,
|
|
1626
|
+
isAdmin: adminParams.isAdmin,
|
|
1627
|
+
hasRank: Boolean(adminParams.rank),
|
|
1628
|
+
});
|
|
1629
|
+
if (dryRun === true) {
|
|
1630
|
+
return (0, core_1.jsonResult)({ ok: true, dryRun: true, accountId: manageAccountId, chatId: adminParams.target });
|
|
1631
|
+
}
|
|
1632
|
+
await requireRuntime().setChatAdmin(adminParams);
|
|
1633
|
+
actionLog.info("clawgram handleAction setAdmin completed", {
|
|
1634
|
+
accountId: manageAccountId,
|
|
1635
|
+
target: adminParams.target,
|
|
1636
|
+
isAdmin: adminParams.isAdmin,
|
|
1637
|
+
});
|
|
1638
|
+
return (0, core_1.jsonResult)({
|
|
1639
|
+
ok: true,
|
|
1640
|
+
accountId: manageAccountId,
|
|
1641
|
+
chatId: adminParams.target,
|
|
1642
|
+
user: adminParams.user,
|
|
1643
|
+
isAdmin: adminParams.isAdmin,
|
|
1644
|
+
...(adminParams.rank ? { rank: adminParams.rank } : {}),
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
if (manageAction === "transferOwnership") {
|
|
1648
|
+
const transferParams = (0, manage_1.parseTransferOwnershipParams)(params, toolContext);
|
|
1649
|
+
requireManagedChat(transferParams.target);
|
|
1650
|
+
actionLog.info("clawgram handleAction transferOwnership", {
|
|
1651
|
+
accountId: manageAccountId,
|
|
1652
|
+
dryRun: dryRun === true,
|
|
1653
|
+
target: transferParams.target,
|
|
1654
|
+
});
|
|
1655
|
+
if (dryRun === true) {
|
|
1656
|
+
return (0, core_1.jsonResult)({ ok: true, dryRun: true, accountId: manageAccountId, chatId: transferParams.target });
|
|
1657
|
+
}
|
|
1658
|
+
const transferGram = requireRuntime();
|
|
1659
|
+
// The password stays inside the runtime: it is read from the
|
|
1660
|
+
// account config at start-up and never travels through dispatch
|
|
1661
|
+
// arguments, which are one log call away from the journal.
|
|
1662
|
+
if (!transferGram.twoFaPassword) {
|
|
1663
|
+
throw new Error("clawgram: ownership transfer requires twoFaPassword in the account config "
|
|
1664
|
+
+ "(the account's Telegram 2FA password, as a literal or a SecretRef)");
|
|
1665
|
+
}
|
|
1666
|
+
await transferGram.transferChatOwnership(transferParams);
|
|
1667
|
+
actionLog.info("clawgram handleAction transferOwnership completed", {
|
|
1668
|
+
accountId: manageAccountId,
|
|
1669
|
+
target: transferParams.target,
|
|
1670
|
+
});
|
|
1671
|
+
return (0, core_1.jsonResult)({
|
|
1672
|
+
ok: true,
|
|
1673
|
+
accountId: manageAccountId,
|
|
1674
|
+
chatId: transferParams.target,
|
|
1675
|
+
newOwner: transferParams.user,
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
// inviteLink — the only management action left.
|
|
1679
|
+
const inviteParams = (0, manage_1.parseInviteLinkParams)(params, toolContext);
|
|
1680
|
+
requireManagedChat(inviteParams.target);
|
|
1681
|
+
actionLog.info("clawgram handleAction inviteLink", {
|
|
1682
|
+
accountId: manageAccountId,
|
|
1683
|
+
dryRun: dryRun === true,
|
|
1684
|
+
target: inviteParams.target,
|
|
1685
|
+
hasExpiry: inviteParams.expireDate !== undefined,
|
|
1686
|
+
usageLimit: inviteParams.usageLimit ?? null,
|
|
1687
|
+
requestNeeded: inviteParams.requestNeeded,
|
|
1688
|
+
});
|
|
1689
|
+
if (dryRun === true) {
|
|
1690
|
+
return (0, core_1.jsonResult)({ ok: true, dryRun: true, accountId: manageAccountId, chatId: inviteParams.target });
|
|
1691
|
+
}
|
|
1692
|
+
const exported = await requireRuntime().exportChatInviteLink(inviteParams);
|
|
1693
|
+
actionLog.info("clawgram handleAction inviteLink completed", {
|
|
1694
|
+
accountId: manageAccountId,
|
|
1695
|
+
target: inviteParams.target,
|
|
1696
|
+
hasLink: Boolean(exported.link),
|
|
1697
|
+
});
|
|
1698
|
+
return (0, core_1.jsonResult)({
|
|
1699
|
+
ok: true,
|
|
1700
|
+
accountId: manageAccountId,
|
|
1701
|
+
chatId: inviteParams.target,
|
|
1702
|
+
link: exported.link,
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1435
1705
|
// Core normalizes whichever of these it filled in to a local path (see
|
|
1436
1706
|
// `mediaSourceParams` above); `mediaUrl` stays a URL, which GramJS
|
|
1437
1707
|
// accepts as well.
|
package/dist/gramjs-client.js
CHANGED
|
@@ -4,10 +4,14 @@ exports.GramJsClientManager = void 0;
|
|
|
4
4
|
exports.buildVoiceNoteParams = buildVoiceNoteParams;
|
|
5
5
|
const telegram_1 = require("telegram");
|
|
6
6
|
const sessions_1 = require("telegram/sessions");
|
|
7
|
+
// Deep import, but the documented one: GramJS ships its SRP helper here and
|
|
8
|
+
// the package has no `exports` field to forbid it.
|
|
9
|
+
const Password_1 = require("telegram/Password");
|
|
7
10
|
const helpers_1 = require("./helpers");
|
|
8
11
|
const proxy_config_1 = require("./proxy-config");
|
|
9
12
|
const secret_refs_1 = require("./secret-refs");
|
|
10
13
|
const history_1 = require("./history");
|
|
14
|
+
const manage_1 = require("./manage");
|
|
11
15
|
function toStringId(value) {
|
|
12
16
|
if (value === null || value === undefined)
|
|
13
17
|
return undefined;
|
|
@@ -562,5 +566,189 @@ class GramJsClientManager {
|
|
|
562
566
|
...buildVoiceNoteParams(args.asVoice),
|
|
563
567
|
});
|
|
564
568
|
}
|
|
569
|
+
// ---- Chat management (2.12.0) ----
|
|
570
|
+
//
|
|
571
|
+
// Transport only: parameter parsing, the manageChats gate and the readers
|
|
572
|
+
// for what these calls return live in `manage.ts`, where they are tested.
|
|
573
|
+
// GramJS resolves EntityLike fields itself (every request below carries a
|
|
574
|
+
// `resolve()` step), so user references travel as the strings the caller
|
|
575
|
+
// supplied — `@username` always works; a bare numeric id only when this
|
|
576
|
+
// account has already seen the user.
|
|
577
|
+
/** The account's 2FA password, if configured — needed only to transfer ownership. */
|
|
578
|
+
get twoFaPassword() {
|
|
579
|
+
const value = this.config.twoFaPassword;
|
|
580
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
581
|
+
}
|
|
582
|
+
/** True when the resolved chat is a supergroup/channel rather than a basic group. */
|
|
583
|
+
isChannelLike(resolved, fallback) {
|
|
584
|
+
const chatType = resolved.chatType ?? inferChatTypeFromRaw(String(resolved.chatId ?? fallback));
|
|
585
|
+
return chatType === "channel";
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* The id a basic-group request wants: positive, without the `-` the rest of
|
|
589
|
+
* this plugin prefixes. Taken from the resolved peer when possible — that is
|
|
590
|
+
* the exact value Telegram handed us.
|
|
591
|
+
*/
|
|
592
|
+
basicChatId(resolved, fallback) {
|
|
593
|
+
const fromPeer = resolved.peer?.chatId;
|
|
594
|
+
if (fromPeer !== undefined && fromPeer !== null) {
|
|
595
|
+
return fromPeer;
|
|
596
|
+
}
|
|
597
|
+
return String(resolved.chatId ?? fallback).replace(/^-/, "");
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Creates a supergroup (megagroup) and invites the initial members.
|
|
601
|
+
*
|
|
602
|
+
* A supergroup rather than a basic group on purpose: granular admin rights,
|
|
603
|
+
* bans and ownership transfer only exist there, and those are the point of
|
|
604
|
+
* managing a chat. Members are added in a second call because
|
|
605
|
+
* `CreateChannel` does not take them; who could not be added (privacy
|
|
606
|
+
* settings) is reported, not thrown — that is an expected outcome and the
|
|
607
|
+
* caller's cue to send an invite link.
|
|
608
|
+
*/
|
|
609
|
+
async createGroup(args) {
|
|
610
|
+
const updates = await this.client.invoke(new telegram_1.Api.channels.CreateChannel({
|
|
611
|
+
title: args.title,
|
|
612
|
+
about: args.about ?? "",
|
|
613
|
+
megagroup: true,
|
|
614
|
+
}));
|
|
615
|
+
const chatId = (0, manage_1.resolveCreatedChannelId)(updates);
|
|
616
|
+
if (args.users.length === 0) {
|
|
617
|
+
return { chatId, missing: [] };
|
|
618
|
+
}
|
|
619
|
+
// The Channel object out of the same Updates is the most reliable handle:
|
|
620
|
+
// it carries the access hash, and needs no entity-cache round trip.
|
|
621
|
+
const createdEntity = (updates?.chats ?? []).find((chat) => chat?.className === "Channel");
|
|
622
|
+
if (!createdEntity) {
|
|
623
|
+
throw new Error("clawgram: group created, but Telegram's answer carried no channel to invite into");
|
|
624
|
+
}
|
|
625
|
+
const invited = await this.client.invoke(new telegram_1.Api.channels.InviteToChannel({
|
|
626
|
+
channel: createdEntity,
|
|
627
|
+
users: args.users,
|
|
628
|
+
}));
|
|
629
|
+
return { chatId, missing: (0, manage_1.summarizeMissingInvitees)(invited) };
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Adds members to an existing chat. Supergroups take the whole list in one
|
|
633
|
+
* call and report who was refused; basic groups only add one user per call,
|
|
634
|
+
* so refusals are collected per user instead of failing the batch.
|
|
635
|
+
*/
|
|
636
|
+
async addChatMembers(args) {
|
|
637
|
+
const resolved = await this.resolvePeer(args.target, { kind: "group" });
|
|
638
|
+
if (this.isChannelLike(resolved, args.target)) {
|
|
639
|
+
const invited = await this.client.invoke(new telegram_1.Api.channels.InviteToChannel({
|
|
640
|
+
channel: resolved.peer,
|
|
641
|
+
users: args.users,
|
|
642
|
+
}));
|
|
643
|
+
return { chatId: resolved.chatId, missing: (0, manage_1.summarizeMissingInvitees)(invited) };
|
|
644
|
+
}
|
|
645
|
+
const chatId = this.basicChatId(resolved, args.target);
|
|
646
|
+
const missing = [];
|
|
647
|
+
for (const user of args.users) {
|
|
648
|
+
try {
|
|
649
|
+
// fwdLimit is how much recent history the newcomer sees; 50 is
|
|
650
|
+
// Telegram's own default in official clients.
|
|
651
|
+
await this.client.invoke(new telegram_1.Api.messages.AddChatUser({
|
|
652
|
+
chatId: chatId,
|
|
653
|
+
userId: user,
|
|
654
|
+
fwdLimit: 50,
|
|
655
|
+
}));
|
|
656
|
+
}
|
|
657
|
+
catch {
|
|
658
|
+
missing.push(user);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return { chatId: resolved.chatId, missing };
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Removes a member. In a supergroup removal is a ban that may then be
|
|
665
|
+
* lifted: lifting it (the default) leaves the person able to come back by
|
|
666
|
+
* invite, keeping the ban makes the removal stick. Basic groups have no ban
|
|
667
|
+
* concept, so there the two collapse into plain removal.
|
|
668
|
+
*/
|
|
669
|
+
async removeChatMember(args) {
|
|
670
|
+
const resolved = await this.resolvePeer(args.target, { kind: "group" });
|
|
671
|
+
if (this.isChannelLike(resolved, args.target)) {
|
|
672
|
+
await this.client.invoke(new telegram_1.Api.channels.EditBanned({
|
|
673
|
+
channel: resolved.peer,
|
|
674
|
+
participant: args.user,
|
|
675
|
+
bannedRights: new telegram_1.Api.ChatBannedRights({ untilDate: 0, viewMessages: true }),
|
|
676
|
+
}));
|
|
677
|
+
if (!args.ban) {
|
|
678
|
+
await this.client.invoke(new telegram_1.Api.channels.EditBanned({
|
|
679
|
+
channel: resolved.peer,
|
|
680
|
+
participant: args.user,
|
|
681
|
+
bannedRights: new telegram_1.Api.ChatBannedRights({ untilDate: 0 }),
|
|
682
|
+
}));
|
|
683
|
+
}
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
await this.client.invoke(new telegram_1.Api.messages.DeleteChatUser({
|
|
687
|
+
chatId: this.basicChatId(resolved, args.target),
|
|
688
|
+
userId: args.user,
|
|
689
|
+
revokeHistory: false,
|
|
690
|
+
}));
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Grants or revokes admin rights. Supergroups take the granular set; basic
|
|
694
|
+
* groups only know a boolean, so the rights collapse to `isAdmin` there.
|
|
695
|
+
*/
|
|
696
|
+
async setChatAdmin(args) {
|
|
697
|
+
const resolved = await this.resolvePeer(args.target, { kind: "group" });
|
|
698
|
+
if (this.isChannelLike(resolved, args.target)) {
|
|
699
|
+
await this.client.invoke(new telegram_1.Api.channels.EditAdmin({
|
|
700
|
+
channel: resolved.peer,
|
|
701
|
+
userId: args.user,
|
|
702
|
+
adminRights: new telegram_1.Api.ChatAdminRights({ ...args.rights }),
|
|
703
|
+
rank: args.rank ?? "",
|
|
704
|
+
}));
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
await this.client.invoke(new telegram_1.Api.messages.EditChatAdmin({
|
|
708
|
+
chatId: this.basicChatId(resolved, args.target),
|
|
709
|
+
userId: args.user,
|
|
710
|
+
isAdmin: args.isAdmin,
|
|
711
|
+
}));
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Hands the chat to a new owner. Telegram demands an SRP proof of the
|
|
715
|
+
* account's 2FA password for this — the one action here that cannot be
|
|
716
|
+
* softened or undone by the old owner, so the proof is the ceremony.
|
|
717
|
+
*
|
|
718
|
+
* The password never leaves this object: it is read from the resolved
|
|
719
|
+
* account config, exchanged for the SRP check, and the check is what goes
|
|
720
|
+
* to Telegram. Callers pass who and where, never the secret.
|
|
721
|
+
*/
|
|
722
|
+
async transferChatOwnership(args) {
|
|
723
|
+
const password = this.twoFaPassword;
|
|
724
|
+
if (!password) {
|
|
725
|
+
throw new Error("clawgram: ownership transfer requires twoFaPassword in the account config");
|
|
726
|
+
}
|
|
727
|
+
const resolved = await this.resolvePeer(args.target, { kind: "group" });
|
|
728
|
+
if (!this.isChannelLike(resolved, args.target)) {
|
|
729
|
+
throw new Error("clawgram: ownership of a basic group cannot be transferred — Telegram only supports this for supergroups");
|
|
730
|
+
}
|
|
731
|
+
const srp = await this.client.invoke(new telegram_1.Api.account.GetPassword());
|
|
732
|
+
const check = await (0, Password_1.computeCheck)(srp, password);
|
|
733
|
+
await this.client.invoke(new telegram_1.Api.channels.EditCreator({
|
|
734
|
+
channel: resolved.peer,
|
|
735
|
+
userId: args.user,
|
|
736
|
+
password: check,
|
|
737
|
+
}));
|
|
738
|
+
}
|
|
739
|
+
/** Issues an invite link — the path for people whose privacy settings refuse a direct add. */
|
|
740
|
+
async exportChatInviteLink(args) {
|
|
741
|
+
const resolved = await this.resolvePeer(args.target, { kind: "group" });
|
|
742
|
+
const exported = await this.client.invoke(new telegram_1.Api.messages.ExportChatInvite({
|
|
743
|
+
peer: resolved.peer,
|
|
744
|
+
...(args.expireDate !== undefined ? { expireDate: args.expireDate } : {}),
|
|
745
|
+
...(args.usageLimit !== undefined ? { usageLimit: args.usageLimit } : {}),
|
|
746
|
+
...(args.title ? { title: args.title } : {}),
|
|
747
|
+
// Presence flips the flag, so the key only exists when asked for —
|
|
748
|
+
// the same lesson the proxy config paid for (see MTProxy note there).
|
|
749
|
+
...(args.requestNeeded ? { requestNeeded: true } : {}),
|
|
750
|
+
}));
|
|
751
|
+
return { link: (0, manage_1.readInviteLink)(exported) };
|
|
752
|
+
}
|
|
565
753
|
}
|
|
566
754
|
exports.GramJsClientManager = GramJsClientManager;
|
package/dist/history.js
CHANGED
|
@@ -18,6 +18,7 @@ exports.parseMessageId = parseMessageId;
|
|
|
18
18
|
exports.parseListMessagesParams = parseListMessagesParams;
|
|
19
19
|
exports.parseListParticipantsParams = parseListParticipantsParams;
|
|
20
20
|
exports.buildHistoryQuery = buildHistoryQuery;
|
|
21
|
+
exports.normalizeChatKey = normalizeChatKey;
|
|
21
22
|
exports.isChatReadable = isChatReadable;
|
|
22
23
|
exports.isWithinWindow = isWithinWindow;
|
|
23
24
|
exports.normalizeHistoryMessage = normalizeHistoryMessage;
|
package/dist/manage.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Chat management — assembling a team chat, as opposed to speaking in it.
|
|
4
|
+
*
|
|
5
|
+
* Creating a group, adding and removing people, appointing admins and handing
|
|
6
|
+
* the chat over are what a manager does with their own account, and the
|
|
7
|
+
* account this plugin drives is exactly that. Telegram's Bot API forbids most
|
|
8
|
+
* of this; MTProto does not, which is why these actions exist here and not in
|
|
9
|
+
* a bot.
|
|
10
|
+
*
|
|
11
|
+
* Everything in this file is pure — parameter parsing, the management scope
|
|
12
|
+
* gate, and readers for the TL shapes the calls return — so it can be tested
|
|
13
|
+
* without a Telegram client. The transport lives in `GramJsClientManager`.
|
|
14
|
+
*
|
|
15
|
+
* The scope gate is deliberately opposite to reading: an absent `readChats`
|
|
16
|
+
* means "read anywhere", an absent `manageChats` means "manage nothing".
|
|
17
|
+
* Reading is what the plugin is for; rearranging chats and kicking people is
|
|
18
|
+
* not something a fresh install may do because nobody said no.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.NO_ADMIN_RIGHTS = exports.DEFAULT_ADMIN_RIGHTS = void 0;
|
|
22
|
+
exports.parseCreateGroupParams = parseCreateGroupParams;
|
|
23
|
+
exports.parseAddMembersParams = parseAddMembersParams;
|
|
24
|
+
exports.parseRemoveMemberParams = parseRemoveMemberParams;
|
|
25
|
+
exports.parsePromoteAdminParams = parsePromoteAdminParams;
|
|
26
|
+
exports.parseDemoteAdminParams = parseDemoteAdminParams;
|
|
27
|
+
exports.parseTransferOwnershipParams = parseTransferOwnershipParams;
|
|
28
|
+
exports.parseInviteLinkParams = parseInviteLinkParams;
|
|
29
|
+
exports.isManagementEnabled = isManagementEnabled;
|
|
30
|
+
exports.isChatManageable = isChatManageable;
|
|
31
|
+
exports.resolveCreatedChannelId = resolveCreatedChannelId;
|
|
32
|
+
exports.summarizeMissingInvitees = summarizeMissingInvitees;
|
|
33
|
+
exports.readInviteLink = readInviteLink;
|
|
34
|
+
const history_1 = require("./history");
|
|
35
|
+
function readString(value) {
|
|
36
|
+
if (typeof value !== "string") {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const trimmed = value.trim();
|
|
40
|
+
return trimmed === "" ? undefined : trimmed;
|
|
41
|
+
}
|
|
42
|
+
/** A user reference: `@username` or a numeric id, as a trimmed string. */
|
|
43
|
+
function readUserRef(value) {
|
|
44
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
45
|
+
return String(Math.trunc(value));
|
|
46
|
+
}
|
|
47
|
+
return readString(value);
|
|
48
|
+
}
|
|
49
|
+
function readTarget(params, toolContext, action) {
|
|
50
|
+
const raw = params.chatId ?? params.target ?? params.to ?? params.chat ?? toolContext?.currentChannelId;
|
|
51
|
+
const target = readString(raw);
|
|
52
|
+
if (!target) {
|
|
53
|
+
throw new Error(`clawgram: ${action} requires a chatId`);
|
|
54
|
+
}
|
|
55
|
+
return target;
|
|
56
|
+
}
|
|
57
|
+
function readUserList(params) {
|
|
58
|
+
const raw = params.users ?? params.members ?? params.user ?? params.userId;
|
|
59
|
+
const entries = Array.isArray(raw) ? raw : raw === undefined || raw === null ? [] : [raw];
|
|
60
|
+
return entries
|
|
61
|
+
.map(readUserRef)
|
|
62
|
+
.filter((entry) => Boolean(entry));
|
|
63
|
+
}
|
|
64
|
+
function readSingleUser(params, action) {
|
|
65
|
+
const user = readUserRef(params.user ?? params.userId ?? params.member);
|
|
66
|
+
if (!user) {
|
|
67
|
+
throw new Error(`clawgram: ${action} requires a user`);
|
|
68
|
+
}
|
|
69
|
+
return user;
|
|
70
|
+
}
|
|
71
|
+
/** Only a literal `true` (or its string form) turns a dangerous flag on. */
|
|
72
|
+
function readFlag(value) {
|
|
73
|
+
return value === true || value === "true";
|
|
74
|
+
}
|
|
75
|
+
function parseCreateGroupParams(params) {
|
|
76
|
+
const title = readString(params.title ?? params.name);
|
|
77
|
+
if (!title) {
|
|
78
|
+
throw new Error("clawgram: createGroup requires a title");
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
title,
|
|
82
|
+
about: readString(params.about ?? params.description),
|
|
83
|
+
users: readUserList(params),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function parseAddMembersParams(params, toolContext) {
|
|
87
|
+
const target = readTarget(params, toolContext, "addMembers");
|
|
88
|
+
const users = readUserList(params);
|
|
89
|
+
if (users.length === 0) {
|
|
90
|
+
throw new Error("clawgram: addMembers requires users to add");
|
|
91
|
+
}
|
|
92
|
+
return { target, users };
|
|
93
|
+
}
|
|
94
|
+
function parseRemoveMemberParams(params, toolContext) {
|
|
95
|
+
return {
|
|
96
|
+
target: readTarget(params, toolContext, "removeMember"),
|
|
97
|
+
user: readSingleUser(params, "removeMember"),
|
|
98
|
+
ban: readFlag(params.ban),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* What a team admin gets: run the room, not the hierarchy. `addAdmins` and
|
|
103
|
+
* `anonymous` are escalation and impersonation respectively, and each must be
|
|
104
|
+
* an explicit choice, never a default.
|
|
105
|
+
*/
|
|
106
|
+
exports.DEFAULT_ADMIN_RIGHTS = Object.freeze({
|
|
107
|
+
changeInfo: true,
|
|
108
|
+
postMessages: false,
|
|
109
|
+
editMessages: false,
|
|
110
|
+
deleteMessages: true,
|
|
111
|
+
banUsers: true,
|
|
112
|
+
inviteUsers: true,
|
|
113
|
+
pinMessages: true,
|
|
114
|
+
addAdmins: false,
|
|
115
|
+
anonymous: false,
|
|
116
|
+
manageCall: true,
|
|
117
|
+
manageTopics: true,
|
|
118
|
+
});
|
|
119
|
+
exports.NO_ADMIN_RIGHTS = Object.freeze({
|
|
120
|
+
changeInfo: false,
|
|
121
|
+
postMessages: false,
|
|
122
|
+
editMessages: false,
|
|
123
|
+
deleteMessages: false,
|
|
124
|
+
banUsers: false,
|
|
125
|
+
inviteUsers: false,
|
|
126
|
+
pinMessages: false,
|
|
127
|
+
addAdmins: false,
|
|
128
|
+
anonymous: false,
|
|
129
|
+
manageCall: false,
|
|
130
|
+
manageTopics: false,
|
|
131
|
+
});
|
|
132
|
+
function parsePromoteAdminParams(params, toolContext) {
|
|
133
|
+
const target = readTarget(params, toolContext, "promoteAdmin");
|
|
134
|
+
const user = readSingleUser(params, "promoteAdmin");
|
|
135
|
+
const rights = { ...exports.DEFAULT_ADMIN_RIGHTS };
|
|
136
|
+
const overrides = params.rights;
|
|
137
|
+
if (overrides && typeof overrides === "object") {
|
|
138
|
+
for (const key of Object.keys(rights)) {
|
|
139
|
+
const value = overrides[key];
|
|
140
|
+
// Only a real boolean overrides: a string "true" here would be a typo'd
|
|
141
|
+
// config, and rights are not the place to guess.
|
|
142
|
+
if (typeof value === "boolean") {
|
|
143
|
+
rights[key] = value;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
target,
|
|
149
|
+
user,
|
|
150
|
+
isAdmin: true,
|
|
151
|
+
rights,
|
|
152
|
+
rank: readString(params.rank ?? params.title),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function parseDemoteAdminParams(params, toolContext) {
|
|
156
|
+
return {
|
|
157
|
+
target: readTarget(params, toolContext, "demoteAdmin"),
|
|
158
|
+
user: readSingleUser(params, "demoteAdmin"),
|
|
159
|
+
isAdmin: false,
|
|
160
|
+
rights: { ...exports.NO_ADMIN_RIGHTS },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function parseTransferOwnershipParams(params, toolContext) {
|
|
164
|
+
return {
|
|
165
|
+
target: readTarget(params, toolContext, "transferOwnership"),
|
|
166
|
+
user: readSingleUser(params, "transferOwnership"),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function parseExpireDate(value) {
|
|
170
|
+
if (value === undefined || value === null || value === "") {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
174
|
+
return Math.trunc(value);
|
|
175
|
+
}
|
|
176
|
+
if (typeof value === "string") {
|
|
177
|
+
const trimmed = value.trim();
|
|
178
|
+
if (/^\d+$/.test(trimmed)) {
|
|
179
|
+
return Number.parseInt(trimmed, 10);
|
|
180
|
+
}
|
|
181
|
+
const parsed = Date.parse(trimmed);
|
|
182
|
+
if (Number.isFinite(parsed)) {
|
|
183
|
+
return Math.floor(parsed / 1000);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// A link that was meant to expire and silently does not is a standing hole
|
|
187
|
+
// in the chat's boundary — refuse rather than degrade.
|
|
188
|
+
throw new Error("clawgram: inviteLink expireDate must be unix seconds or an ISO date");
|
|
189
|
+
}
|
|
190
|
+
function parseUsageLimit(value) {
|
|
191
|
+
if (value === undefined || value === null || value === "") {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
const parsed = typeof value === "number" ? value : Number(String(value).trim());
|
|
195
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
196
|
+
throw new Error("clawgram: inviteLink usageLimit must be a positive number");
|
|
197
|
+
}
|
|
198
|
+
return Math.trunc(parsed);
|
|
199
|
+
}
|
|
200
|
+
function parseInviteLinkParams(params, toolContext) {
|
|
201
|
+
return {
|
|
202
|
+
target: readTarget(params, toolContext, "inviteLink"),
|
|
203
|
+
expireDate: parseExpireDate(params.expireDate ?? params.expiresAt),
|
|
204
|
+
usageLimit: parseUsageLimit(params.usageLimit ?? params.memberLimit),
|
|
205
|
+
title: readString(params.title ?? params.label),
|
|
206
|
+
requestNeeded: readFlag(params.requestNeeded ?? params.requireApproval),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function normalizeScope(manageChats) {
|
|
210
|
+
if (manageChats === undefined || manageChats === null) {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
return (Array.isArray(manageChats) ? manageChats : [manageChats])
|
|
214
|
+
.map(history_1.normalizeChatKey)
|
|
215
|
+
.filter(Boolean);
|
|
216
|
+
}
|
|
217
|
+
/** True while the account is allowed to manage anything at all. */
|
|
218
|
+
function isManagementEnabled(manageChats) {
|
|
219
|
+
return normalizeScope(manageChats).length > 0;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The management gate. Absent, null and empty all deny: management is opt-in,
|
|
223
|
+
* and the wildcard has to be written down to mean "everywhere".
|
|
224
|
+
*/
|
|
225
|
+
function isChatManageable(target, manageChats) {
|
|
226
|
+
const entries = normalizeScope(manageChats);
|
|
227
|
+
if (entries.length === 0) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
if (entries.includes("*")) {
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
return entries.includes((0, history_1.normalizeChatKey)(target));
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* The id of the supergroup a `channels.CreateChannel` call just created, read
|
|
237
|
+
* from the `Updates` it returns, in the `-100…` form the rest of this plugin
|
|
238
|
+
* speaks. GramJS carries ids as `big-integer` objects as often as native
|
|
239
|
+
* numbers, so the id goes through `String` rather than arithmetic.
|
|
240
|
+
*/
|
|
241
|
+
function resolveCreatedChannelId(updates) {
|
|
242
|
+
const chats = updates?.chats;
|
|
243
|
+
if (!Array.isArray(chats)) {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
for (const chat of chats) {
|
|
247
|
+
if (chat?.className !== "Channel") {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const id = chat?.id;
|
|
251
|
+
if (id === undefined || id === null) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const asString = String(id).trim();
|
|
255
|
+
if (asString && asString !== "[object Object]") {
|
|
256
|
+
return `-100${asString.replace(/^-100|-/, "")}`;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Who Telegram refused to add, as user ids. Privacy settings make this an
|
|
263
|
+
* expected outcome, not an error: the caller is told exactly who still needs
|
|
264
|
+
* an invite link instead of the whole call failing.
|
|
265
|
+
*/
|
|
266
|
+
function summarizeMissingInvitees(result) {
|
|
267
|
+
const missing = result?.missingInvitees;
|
|
268
|
+
if (!Array.isArray(missing)) {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
const ids = [];
|
|
272
|
+
for (const entry of missing) {
|
|
273
|
+
const id = entry?.userId;
|
|
274
|
+
if (id === undefined || id === null) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const asString = String(id).trim();
|
|
278
|
+
if (asString && asString !== "[object Object]") {
|
|
279
|
+
ids.push(asString);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return ids;
|
|
283
|
+
}
|
|
284
|
+
/** The link out of a `TypeExportedChatInvite`, or nothing when there is none. */
|
|
285
|
+
function readInviteLink(result) {
|
|
286
|
+
return readString(result?.link);
|
|
287
|
+
}
|
package/dist/secret-refs.js
CHANGED
|
@@ -24,7 +24,7 @@ exports.applyAccountSecrets = applyAccountSecrets;
|
|
|
24
24
|
exports.hasUnresolvedSecretRef = hasUnresolvedSecretRef;
|
|
25
25
|
exports.readSecretInput = readSecretInput;
|
|
26
26
|
/** Credential fields that accept a reference, in the order they are reported. */
|
|
27
|
-
const ACCOUNT_SECRET_FIELDS = ["apiHash", "sessionString"];
|
|
27
|
+
const ACCOUNT_SECRET_FIELDS = ["apiHash", "sessionString", "twoFaPassword"];
|
|
28
28
|
const PROXY_SECRET_FIELDS = ["username", "password"];
|
|
29
29
|
/**
|
|
30
30
|
* Must match OpenClaw's own keying, or every lookup misses and a correctly
|
package/dist/silent-reaction.js
CHANGED
|
@@ -46,8 +46,19 @@ function buildEmojiSystemPrompt(appetite, allowed) {
|
|
|
46
46
|
`- the message is about producing something written — a text, a reply, a document, a draft → ✍`,
|
|
47
47
|
"Otherwise match the mood: a joke gets something amused, bad news something",
|
|
48
48
|
"sympathetic, an achievement something celebratory.",
|
|
49
|
-
"Answer NONE when the message is conflictual
|
|
50
|
-
"
|
|
49
|
+
"Answer NONE when the message is conflictual or heavy, or when it judges",
|
|
50
|
+
"SOMEONE ELSE's work or behaviour — a reaction there reads as a verdict on",
|
|
51
|
+
"that person.",
|
|
52
|
+
// Without this the NONE rule swallowed the praise rule: "молодец тина" is
|
|
53
|
+
// literally a statement about someone's performance, the model applied the
|
|
54
|
+
// rule as written, and answered NONE to being praised. The carve-out
|
|
55
|
+
// exists in the assistant's own rules — the ban on judging people protects
|
|
56
|
+
// others, not itself — and had to be repeated here.
|
|
57
|
+
"That restraint protects other people and does NOT apply to the assistant",
|
|
58
|
+
"itself. Praise, teasing, thanks or criticism aimed at the assistant is",
|
|
59
|
+
"exactly what the fixed answers above are for: being called clever, useful",
|
|
60
|
+
"or 'молодец' is praise → ❤, not a verdict on a third party.",
|
|
61
|
+
"When a fixed answer applies, it wins over the mood rule.",
|
|
51
62
|
];
|
|
52
63
|
return appetite === "minimal"
|
|
53
64
|
? [
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "clawgram",
|
|
3
3
|
"name": "Clawgram",
|
|
4
4
|
"description": "Clawgram — personal Telegram (MTProto userbot) channel for OpenClaw. Your AI assistant reads and responds as you.",
|
|
5
|
-
"version": "2.
|
|
5
|
+
"version": "2.12.1",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
|
@@ -322,6 +322,47 @@
|
|
|
322
322
|
},
|
|
323
323
|
"description": "Chat ids the account may read history and membership for. Absent means no restriction; an empty array denies everything."
|
|
324
324
|
},
|
|
325
|
+
"manageChats": {
|
|
326
|
+
"type": "array",
|
|
327
|
+
"items": {
|
|
328
|
+
"type": "string"
|
|
329
|
+
},
|
|
330
|
+
"description": "Chat ids the account may MANAGE (2.12.0): add/remove members, promote/demote admins, transfer ownership, export invite links; a non-empty list also unlocks createGroup. Opposite default to readChats: absent or empty means chat management is off, [\"*\"] allows every chat."
|
|
331
|
+
},
|
|
332
|
+
"twoFaPassword": {
|
|
333
|
+
"description": "The account's Telegram 2FA (cloud) password — needed only by transferOwnership, which Telegram guards with an SRP proof. Literal value, or a SecretRef resolved at account start-up.",
|
|
334
|
+
"anyOf": [
|
|
335
|
+
{
|
|
336
|
+
"type": "string"
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
"type": "object",
|
|
340
|
+
"required": [
|
|
341
|
+
"source",
|
|
342
|
+
"provider",
|
|
343
|
+
"id"
|
|
344
|
+
],
|
|
345
|
+
"additionalProperties": false,
|
|
346
|
+
"properties": {
|
|
347
|
+
"source": {
|
|
348
|
+
"enum": [
|
|
349
|
+
"env",
|
|
350
|
+
"file",
|
|
351
|
+
"exec"
|
|
352
|
+
]
|
|
353
|
+
},
|
|
354
|
+
"provider": {
|
|
355
|
+
"type": "string",
|
|
356
|
+
"pattern": "^[a-z][a-z0-9_-]{0,63}$"
|
|
357
|
+
},
|
|
358
|
+
"id": {
|
|
359
|
+
"type": "string",
|
|
360
|
+
"minLength": 1
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
]
|
|
365
|
+
},
|
|
325
366
|
"joinsJournalPath": {
|
|
326
367
|
"type": "string",
|
|
327
368
|
"description": "Where joins observed for this account are journalled. Defaults to a per-account file under the OpenClaw state directory."
|
|
@@ -356,6 +397,17 @@
|
|
|
356
397
|
}
|
|
357
398
|
},
|
|
358
399
|
"uiHints": {
|
|
400
|
+
"accounts.*.manageChats": {
|
|
401
|
+
"label": "Managed Chats",
|
|
402
|
+
"help": "Chat ids the assistant may manage (create groups, add/remove members, admins, ownership, invite links). Leave empty to keep chat management off; use * to allow every chat.",
|
|
403
|
+
"advanced": true
|
|
404
|
+
},
|
|
405
|
+
"accounts.*.twoFaPassword": {
|
|
406
|
+
"label": "2FA Password",
|
|
407
|
+
"help": "The account's Telegram two-step verification password. Only needed for ownership transfer.",
|
|
408
|
+
"sensitive": true,
|
|
409
|
+
"advanced": true
|
|
410
|
+
},
|
|
359
411
|
"accounts.*.proxy": {
|
|
360
412
|
"label": "SOCKS Proxy",
|
|
361
413
|
"help": "Optional native SOCKS4/SOCKS5 proxy for this account's MTProto connection. GramJS uses raw TCP sockets, so OpenClaw's proxy.proxyUrl does not cover it. This is a SOCKS proxy, not a Telegram MTProxy.",
|
package/package.json
CHANGED