clawgram 2.25.0 → 2.26.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 CHANGED
@@ -714,6 +714,35 @@ Bindings
714
714
  ```
715
715
 
716
716
 
717
+ ## Actions and the names that reach them
718
+
719
+ Two grammars name the same actions. The **native** names (`read`, `participants`, `topics`,
720
+ `dialogs`, `chatInfo`, `fetch-media`, `createGroup`, …) are what the gateway RPC, the tests and
721
+ this README use. The agent's `message` tool, however, only dispatches names from **core's own
722
+ vocabulary** (`CHANNEL_MESSAGE_ACTION_NAMES`), so every action worth reaching from a prompt also
723
+ answers to core's nearest name. `src/actions.ts` is the single source of both lists
724
+ (`ACTION_ALIASES`, `CORE_VOCABULARY_SPELLINGS`); that core really knows each name is asserted
725
+ against the installed core in `test/core-action-synonyms.test.ts`, so a core release that drops
726
+ one fails the suite rather than the chat.
727
+
728
+ | Does | Native name | Callable from the `message` tool as | Gate |
729
+ |---|---|---|---|
730
+ | send text | `send` | `send` | `sendChats` |
731
+ | send a file | `upload-file` | `upload-file`, `sendAttachment` | `sendChats`, media roots |
732
+ | react to a message | `react` | `react` | `sendChats` |
733
+ | read history | `read` (also `list`) | `read` | `readChats` |
734
+ | fetch an attachment | `fetch-media` | `download-file` | `readChats` |
735
+ | list members | `participants` | `member-info` | `readChats` |
736
+ | list forum topics | `topics` | `thread-list` | `readChats` |
737
+ | list chats | `dialogs` | `channel-list` | `discoverChats` |
738
+ | describe a chat | `chatInfo` | `channel-info` (the chat arrives in `channelId`) | `readChats` |
739
+ | where the account was added | `joins` | — (gateway RPC only) | — |
740
+ | chat management | see the table below | see the table below | `manageChats` |
741
+
742
+ Names outside core's vocabulary (`joins`, `transferOwnership`, `inviteLink`) are reachable through
743
+ the gateway RPC only. A name core does not know fails as "requires a target" and "does not accept a
744
+ target" at once — there is no call that satisfies both, which is why this table exists.
745
+
717
746
  ## Chat management
718
747
 
719
748
  Since 2.12.0 the assistant can assemble a chat, not only speak in it: create a supergroup, add and
@@ -728,15 +757,15 @@ management to those chats, `["*"]` allows every chat. A non-empty list also unlo
728
757
  (the chat being created is not in any list yet). All actions honour `dryRun`, and the gate is
729
758
  checked before the dry-run answer, so a dry run exercises the same refusals a real call would hit.
730
759
 
731
- | Action | Parameters | Notes |
732
- |---|---|---|
733
- | `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` |
734
- | `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 |
735
- | `removeMember` | `chatId`, `user`, `ban?` | Soft kick by default — the person may be re-invited later. `ban: true` keeps them out until unbanned |
736
- | `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` |
737
- | `demoteAdmin` | `chatId`, `user` | Strips every admin right |
738
- | `transferOwnership` | `chatId`, `user` | Supergroups only. Requires `twoFaPassword` (below); Telegram's own rules surface as errors — see the fine print |
739
- | `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 |
760
+ | Action | Callable from the `message` tool as | Parameters | Notes |
761
+ |---|---|---|---|
762
+ | `createGroup` | `channel-create` | `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` |
763
+ | `addMembers` | `addParticipant` | `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 |
764
+ | `removeMember` | `kick` | `chatId`, `user`, `ban?` | Soft kick by default — the person may be re-invited later. `ban: true` keeps them out until unbanned |
765
+ | `promoteAdmin` | `role-add` | `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` |
766
+ | `demoteAdmin` | `role-remove` | `chatId`, `user` | Strips every admin right |
767
+ | `transferOwnership` | — (gateway RPC only) | `chatId`, `user` | Supergroups only. Requires `twoFaPassword` (below); Telegram's own rules surface as errors — see the fine print |
768
+ | `inviteLink` | — (gateway RPC only) | `chatId`, `expireDate?`, `usageLimit?`, `title?`, `requestNeeded?` | The path for people whose privacy settings refuse a direct add. `expireDate` takes unix seconds or an ISO date |
740
769
 
741
770
  User references in `users`/`user` are `@username` or a numeric Telegram id. A `@username` always
742
771
  resolves; a bare numeric id only when the account has already seen the user (shared chat, dialog,
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.rememberAccount = rememberAccount;
4
+ exports.forgetAccount = forgetAccount;
5
+ exports.sendScopeFor = sendScopeFor;
6
+ exports.operatorIdsFor = operatorIdsFor;
7
+ exports.requireRuntime = requireRuntime;
8
+ const records = new Map();
9
+ function rememberAccount(accountId, record) {
10
+ records.set(accountId, { sendChats: record.sendChats, operatorIds: [...record.operatorIds] });
11
+ }
12
+ function forgetAccount(accountId) {
13
+ records.delete(accountId);
14
+ }
15
+ function sendScopeFor(accountId) {
16
+ return records.get(accountId)?.sendChats;
17
+ }
18
+ function operatorIdsFor(accountId) {
19
+ return records.get(accountId)?.operatorIds ?? [];
20
+ }
21
+ /**
22
+ * The connected runtime for an account, or a refusal naming it.
23
+ *
24
+ * One helper instead of the copies of this three-liner that used to sit in
25
+ * each dispatch branch (A6-11 removed six; D2-11 the remaining seven).
26
+ */
27
+ function requireRuntime(runtimes, accountId) {
28
+ const gram = runtimes.get(accountId);
29
+ if (!gram) {
30
+ throw new Error(`clawgram: runtime not found for account ${accountId}`);
31
+ }
32
+ return gram;
33
+ }
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readAccountReadChats = readAccountReadChats;
4
+ exports.resolveAccountReadChats = resolveAccountReadChats;
5
+ exports.resolveAccountSendChats = resolveAccountSendChats;
6
+ exports.refuseOutboundOutsideScope = refuseOutboundOutsideScope;
7
+ exports.resolveAccountOperatorIds = resolveAccountOperatorIds;
8
+ exports.resolveAccountDiscoverChats = resolveAccountDiscoverChats;
9
+ exports.resolveAccountManageChats = resolveAccountManageChats;
10
+ exports.readAccountManageChats = readAccountManageChats;
11
+ const core_1 = require("openclaw/plugin-sdk/core");
12
+ const history_1 = require("./history");
13
+ const send_scope_1 = require("./send-scope");
14
+ /**
15
+ * Per-account scope as configured — the readers every action module shares.
16
+ *
17
+ * Lived at the top of `channel.ts` next to the dispatcher that used them;
18
+ * moved out with the dispatcher's branches (audit B5-13, part 3). Pure
19
+ * config readers: no runtime, no state.
20
+ */
21
+ const actionLog = (0, core_1.createSubsystemLogger)("channels/clawgram");
22
+ /**
23
+ * Read scope as configured for the account. Left `undefined` when the key is
24
+ * absent so `isChatReadable` can tell "not configured" from "configured empty" —
25
+ * the first means no restriction, the second denies everything.
26
+ */
27
+ function readAccountReadChats(account) {
28
+ return (0, history_1.normalizeScopeList)(account?.readChats);
29
+ }
30
+ function resolveAccountReadChats(cfg, accountId) {
31
+ return readAccountReadChats(cfg?.channels?.["clawgram"]?.accounts?.[accountId]);
32
+ }
33
+ /**
34
+ * Outbound scope as configured. Handed to `isChatSendable` raw: an absent
35
+ * value means "unrestricted" and an empty list means "deny", and only the
36
+ * raw value tells those apart — same shape as `readChats`.
37
+ */
38
+ function resolveAccountSendChats(cfg, accountId) {
39
+ return cfg?.channels?.["clawgram"]?.accounts?.[accountId]?.sendChats;
40
+ }
41
+ /** One refusal for every outbound action, so the three read the same. */
42
+ function refuseOutboundOutsideScope(action, accountId, target) {
43
+ const refusal = (0, send_scope_1.describeSendRefusal)(target);
44
+ actionLog.warn(`clawgram ${action} refused: ${refusal.reason}`, { accountId, ...refusal.logFields });
45
+ throw refusal.error;
46
+ }
47
+ /**
48
+ * Who receives core's operational telemetry in a DM.
49
+ */
50
+ function resolveAccountOperatorIds(cfg, accountId) {
51
+ const account = cfg?.channels?.["clawgram"]?.accounts?.[accountId];
52
+ // Только явный список. Умолчание «operatorIds = allowFrom» делало
53
+ // оператором каждого допущенного собеседника — и телеметрию с путями
54
+ // secret-store получал любой из них (D2-03, A5-11). Не назван — не
55
+ // назван: уведомления подавляются везде.
56
+ const raw = account?.operatorIds;
57
+ if (raw === undefined || raw === null)
58
+ return [];
59
+ const entries = Array.isArray(raw) ? raw : [raw];
60
+ return entries.map((entry) => String(entry).trim()).filter(Boolean);
61
+ }
62
+ function resolveAccountDiscoverChats(cfg, accountId) {
63
+ return cfg?.channels?.["clawgram"]?.accounts?.[accountId]?.discoverChats;
64
+ }
65
+ /**
66
+ * Management scope as configured. Handed to `isChatManageable` raw: unlike
67
+ * `readChats`, an absent value already means "deny", so there is nothing to
68
+ * tell apart — but the raw value keeps the two gates symmetrical.
69
+ */
70
+ function resolveAccountManageChats(cfg, accountId) {
71
+ return cfg?.channels?.["clawgram"]?.accounts?.[accountId]?.manageChats;
72
+ }
73
+ /** Same normalization `readChats` gets, for the resolved-account copy. */
74
+ function readAccountManageChats(account) {
75
+ return (0, history_1.normalizeScopeList)(account?.manageChats);
76
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleManageAction = handleManageAction;
4
+ const core_1 = require("openclaw/plugin-sdk/core");
5
+ const account_scopes_1 = require("./account-scopes");
6
+ const actions_1 = require("./actions");
7
+ const manage_1 = require("./manage");
8
+ /**
9
+ * The chat-management actions, gated by `manageChats`.
10
+ *
11
+ * Cut out of `handleAction` in `channel.ts` unchanged (audit B5-13, part 3);
12
+ * the probe is the proof. Answers `undefined` for any other action.
13
+ */
14
+ const actionLog = (0, core_1.createSubsystemLogger)("channels/clawgram");
15
+ async function handleManageAction(ctx) {
16
+ const { canonical, params, cfg, accountId, dryRun, toolContext, resolveRuntimeAccountId, requireRuntimeFor } = ctx;
17
+ // ---- Chat management (2.12.0) ----
18
+ //
19
+ // Assembling a chat rather than speaking in it: create a supergroup,
20
+ // add and remove people, appoint admins, hand the chat over, issue an
21
+ // invite link. All of it is possible only because this is a personal
22
+ // MTProto account — a bot could do almost none of this.
23
+ //
24
+ // Every branch is gated by the account's `manageChats` scope, which is
25
+ // opt-in (absent = deny, see manage.ts) — these are the first actions
26
+ // that change a chat rather than write into it. Parsing runs before
27
+ // the gate so a malformed call fails on its own shape, and `dryRun`
28
+ // returns after the gate so a dry run exercises the same refusals a
29
+ // real call would hit. People's ids stay out of the logs throughout;
30
+ // the JSON result carries them to the caller, the journal does not.
31
+ const manageAction = actions_1.MANAGE_ACTIONS.has(canonical) ? canonical : undefined;
32
+ if (manageAction) {
33
+ const manageAccountId = resolveRuntimeAccountId(cfg, accountId);
34
+ if (!manageAccountId) {
35
+ throw new Error("clawgram: no configured account found");
36
+ }
37
+ const manageScope = (0, account_scopes_1.resolveAccountManageChats)(cfg, manageAccountId);
38
+ const requireRuntime = () => requireRuntimeFor(manageAccountId);
39
+ /**
40
+ * The scaffold every management action shares.
41
+ *
42
+ * Six actions used to spell it out one after another: resolve the
43
+ * account, check the scope, log, answer a dry run, call the
44
+ * runtime, log again, build the result. A change to any of those —
45
+ * the dry-run contract, say — was a six-place edit in the plugin's
46
+ * largest file, and the one deliberate exception (createGroup does
47
+ * not check a chat scope, because the chat does not exist yet) was
48
+ * invisible among the copies (finding A12-06).
49
+ *
50
+ * The differences stay written at each call site: what to parse,
51
+ * what to log, what to run, what to answer. Only the scaffold moved.
52
+ */
53
+ const runManage = async (spec) => {
54
+ const parsed = spec.parse();
55
+ const target = spec.target(parsed);
56
+ if (target === undefined) {
57
+ // Nothing to check a scope against yet, so the gate is coarser:
58
+ // management must be enabled at all for this account.
59
+ if (!(0, manage_1.isManagementEnabled)(manageScope)) {
60
+ actionLog.warn(`clawgram ${spec.name} refused: management is not enabled`, {
61
+ accountId: manageAccountId,
62
+ });
63
+ throw new Error("clawgram: chat management is not enabled for this account — "
64
+ + `set channels.clawgram.accounts.${manageAccountId}.manageChats`);
65
+ }
66
+ }
67
+ else if (!(0, manage_1.isChatManageable)(target, manageScope)) {
68
+ actionLog.warn("clawgram management refused: chat outside manage scope", {
69
+ accountId: manageAccountId,
70
+ action: manageAction,
71
+ target,
72
+ });
73
+ throw new Error(`clawgram: not-managed-chat ${target}`);
74
+ }
75
+ actionLog.info(`clawgram handleAction ${spec.name}`, {
76
+ accountId: manageAccountId,
77
+ dryRun: dryRun === true,
78
+ ...spec.before(parsed),
79
+ });
80
+ if (dryRun === true) {
81
+ return (0, core_1.jsonResult)({
82
+ ok: true,
83
+ dryRun: true,
84
+ accountId: manageAccountId,
85
+ ...(target === undefined ? {} : { chatId: target }),
86
+ });
87
+ }
88
+ const gram = requireRuntime();
89
+ spec.precondition?.(gram);
90
+ const result = await spec.run(gram, parsed);
91
+ actionLog.info(`clawgram handleAction ${spec.name} completed`, {
92
+ accountId: manageAccountId,
93
+ ...spec.after(parsed, result),
94
+ });
95
+ return (0, core_1.jsonResult)({ ok: true, accountId: manageAccountId, ...spec.result(parsed, result) });
96
+ };
97
+ if (manageAction === "createGroup") {
98
+ return await runManage({
99
+ name: "createGroup",
100
+ parse: () => (0, manage_1.parseCreateGroupParams)(params),
101
+ // A group being created is not in any scope yet.
102
+ target: () => undefined,
103
+ before: (p) => ({ users: p.users.length, hasAbout: Boolean(p.about) }),
104
+ run: (gram, p) => gram.createGroup(p),
105
+ after: (_p, created) => ({ chatId: created.chatId ?? null, missing: created.missing.length }),
106
+ result: (_p, created) => ({ chatId: created.chatId, missing: created.missing }),
107
+ });
108
+ }
109
+ if (manageAction === "addMembers") {
110
+ return await runManage({
111
+ name: "addMembers",
112
+ parse: () => (0, manage_1.parseAddMembersParams)(params, toolContext),
113
+ target: (p) => p.target,
114
+ before: (p) => ({ target: p.target, users: p.users.length }),
115
+ run: (gram, p) => gram.addChatMembers(p),
116
+ after: (p, added) => ({
117
+ target: p.target,
118
+ requested: p.users.length,
119
+ missing: added.missing.length,
120
+ }),
121
+ result: (p, added) => ({
122
+ chatId: added.chatId ?? p.target,
123
+ requested: p.users.length,
124
+ // Telegram refuses silently-restricted invites per user; the
125
+ // caller gets the ids so it can hand them an invite link.
126
+ missing: added.missing,
127
+ }),
128
+ });
129
+ }
130
+ if (manageAction === "removeMember") {
131
+ return await runManage({
132
+ name: "removeMember",
133
+ parse: () => (0, manage_1.parseRemoveMemberParams)(params, toolContext),
134
+ target: (p) => p.target,
135
+ before: (p) => ({ target: p.target, ban: p.ban }),
136
+ run: (gram, p) => gram.removeChatMember(p),
137
+ after: (p) => ({ target: p.target, ban: p.ban }),
138
+ result: (p) => ({ chatId: p.target, user: p.user, banned: p.ban }),
139
+ });
140
+ }
141
+ if (manageAction === "promoteAdmin" || manageAction === "demoteAdmin") {
142
+ const promote = manageAction === "promoteAdmin";
143
+ return await runManage({
144
+ // Both spellings log as `setAdmin`, as they always have.
145
+ name: "setAdmin",
146
+ parse: () => (promote
147
+ ? (0, manage_1.parsePromoteAdminParams)(params, toolContext)
148
+ : (0, manage_1.parseDemoteAdminParams)(params, toolContext)),
149
+ target: (p) => p.target,
150
+ before: (p) => ({ target: p.target, isAdmin: p.isAdmin, hasRank: Boolean(p.rank) }),
151
+ run: (gram, p) => gram.setChatAdmin(p),
152
+ after: (p) => ({ target: p.target, isAdmin: p.isAdmin }),
153
+ result: (p) => ({
154
+ chatId: p.target,
155
+ user: p.user,
156
+ isAdmin: p.isAdmin,
157
+ ...(p.rank ? { rank: p.rank } : {}),
158
+ }),
159
+ });
160
+ }
161
+ if (manageAction === "transferOwnership") {
162
+ return await runManage({
163
+ name: "transferOwnership",
164
+ parse: () => (0, manage_1.parseTransferOwnershipParams)(params, toolContext),
165
+ target: (p) => p.target,
166
+ before: (p) => ({ target: p.target }),
167
+ // The password stays inside the runtime: it is read from the
168
+ // account config at start-up and never travels through dispatch
169
+ // arguments, which are one log call away from the journal.
170
+ precondition: (gram) => {
171
+ if (!gram.twoFaPassword) {
172
+ throw new Error("clawgram: ownership transfer requires twoFaPassword in the account config "
173
+ + "(the account's Telegram 2FA password, as a literal or a SecretRef)");
174
+ }
175
+ },
176
+ run: (gram, p) => gram.transferChatOwnership(p),
177
+ after: (p) => ({ target: p.target }),
178
+ result: (p) => ({ chatId: p.target, newOwner: p.user }),
179
+ });
180
+ }
181
+ // inviteLink — the only management action left.
182
+ return await runManage({
183
+ name: "inviteLink",
184
+ parse: () => (0, manage_1.parseInviteLinkParams)(params, toolContext),
185
+ target: (p) => p.target,
186
+ before: (p) => ({
187
+ target: p.target,
188
+ hasExpiry: p.expireDate !== undefined,
189
+ usageLimit: p.usageLimit ?? null,
190
+ requestNeeded: p.requestNeeded,
191
+ }),
192
+ run: (gram, p) => gram.exportChatInviteLink(p),
193
+ after: (p, exported) => ({ target: p.target, hasLink: Boolean(exported.link) }),
194
+ result: (p, exported) => ({ chatId: p.target, link: exported.link }),
195
+ });
196
+ }
197
+ return undefined;
198
+ }