clawgram 2.28.1 → 2.29.0

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
@@ -31,6 +31,7 @@ Clawgram is a personal-Telegram channel plugin for [OpenClaw](https://github.com
31
31
  - **@Mention detection** — respond only when mentioned in groups (text, caption, and ID-based mentions)
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
+ - **Editing what was sent** — rewrite the account's own message in place instead of posting a correction beneath it (`edit` action)
34
35
  - **Chat metadata** — title, type, member count, description, forum flag and pinned message (`chatInfo` action)
35
36
  - **Attachments on demand** — fetch the photo or voice note on any message in read scope, as a reading, a file, or both (`fetch-media` action)
36
37
  - **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
@@ -260,7 +261,7 @@ loud where it does occur.
260
261
  | `groups` | object | `{}` | Allowed groups map keyed by explicit group id or `*` |
261
262
  | `proxy` | object | unset | Optional SOCKS4/SOCKS5 proxy for this account — see [Proxy (SOCKS4/SOCKS5)](#proxy-socks4socks5) |
262
263
  | `manageChats` | string[] | unset | Chats the assistant may **manage** — see [Chat management](#chat-management). Absent or empty = management off; `["*"]` = every chat |
263
- | `sendChats` | string[] | unset | Chats the assistant may **send to** — `send`, `upload-file`, `react` and core's own delivery path (`--deliver`, sub-agent announcements). Absent = every chat; `[]` = none. Phone-number targets are refused regardless (2.18.0; core delivery covered since 2.25.0) |
264
+ | `sendChats` | string[] | unset | Chats the assistant may **send to** — `send`, `upload-file`, `react`, `edit` and core's own delivery path (`--deliver`, sub-agent announcements). Absent = every chat; `[]` = none. Phone-number targets are refused regardless (2.18.0; core delivery covered since 2.25.0) |
264
265
  | `replyParseMode` | `"html"` \| `"markdown"` \| `"none"` | unset | Outbound format for replies, core-delivered text, captions and `send` calls that omit `parseMode` — see [Message formatting](#message-formatting) |
265
266
  | `twoFaPassword` | string \| SecretRef | unset | The account's Telegram 2FA password; read only by `transferOwnership` |
266
267
  | `reactionModel` | string | unset | Model ref or alias for the emoji pick on a silent mention. Unset = the agent's own model. Needs `plugins.entries.clawgram.llm.allowModelOverride: true` in the gateway config; without it the override is refused and the pick quietly falls back to the default model |
@@ -4,6 +4,7 @@ exports.handleSendAction = handleSendAction;
4
4
  const core_1 = require("openclaw/plugin-sdk/core");
5
5
  const param_readers_1 = require("openclaw/plugin-sdk/param-readers");
6
6
  const account_scopes_1 = require("./account-scopes");
7
+ const edits_1 = require("./edits");
7
8
  const group_reply_address_1 = require("./group-reply-address");
8
9
  const group_visible_reply_guard_1 = require("./group-visible-reply-guard");
9
10
  const helpers_1 = require("./helpers");
@@ -62,6 +63,66 @@ async function handleSendAction(ctx) {
62
63
  removed: reactionParams.remove,
63
64
  });
64
65
  }
66
+ // Rewriting a message already sent. Gated exactly like `react`: it is a
67
+ // visible act in someone else's chat under this account's name, so it takes
68
+ // the outbound scope check and honours `dryRun`, which reading does not need.
69
+ if (canonical === "edit") {
70
+ const editParams = (0, edits_1.parseEditParams)(params, toolContext);
71
+ const editAccountId = resolveRuntimeAccountId(cfg, accountId);
72
+ if (!editAccountId) {
73
+ throw new Error("clawgram: no configured account found");
74
+ }
75
+ if (!(0, send_scope_1.isChatSendable)(editParams.target, (0, account_scopes_1.resolveAccountSendChats)(cfg, editAccountId))) {
76
+ (0, account_scopes_1.refuseOutboundOutsideScope)("edit", editAccountId, editParams.target);
77
+ }
78
+ // The same sentinel guard as `send`, for the same reason: `NO_REPLY` is
79
+ // OpenClaw's "say nothing", and an explicit tool call is not a path that
80
+ // strips it. Editing a real answer down to the token would replace a good
81
+ // message with what looks like a malfunction — worse than the send case,
82
+ // because the original text is gone.
83
+ if ((0, helpers_1.isSilentReplyText)(editParams.text)) {
84
+ throw new Error("clawgram: edit refuses a silent-reply sentinel as the new text");
85
+ }
86
+ // No reply-address prefix, no visible-reply memory, no turn-send memory:
87
+ // those three exist to shape and de-duplicate NEW messages. An edit
88
+ // addresses nobody afresh and adds nothing to the chat, so recording it as
89
+ // "this turn has spoken" would suppress a later genuine answer.
90
+ const editParseMode = (0, helpers_1.resolveOutboundParseMode)(params, cfg, editAccountId);
91
+ actionLog.info("clawgram handleAction edit", {
92
+ accountId: editAccountId,
93
+ dryRun: dryRun === true,
94
+ target: editParams.target,
95
+ messageId: editParams.messageId,
96
+ });
97
+ if (dryRun === true) {
98
+ return (0, core_1.jsonResult)({
99
+ ok: true,
100
+ dryRun: true,
101
+ accountId: editAccountId,
102
+ chatId: editParams.target,
103
+ messageId: editParams.messageId,
104
+ });
105
+ }
106
+ const editGram = requireRuntimeFor(editAccountId);
107
+ await editGram.editText({
108
+ target: editParams.target,
109
+ messageId: editParams.messageId,
110
+ text: editParams.text,
111
+ parseMode: editParseMode,
112
+ });
113
+ actionLog.info("clawgram handleAction edit completed", {
114
+ accountId: editAccountId,
115
+ target: editParams.target,
116
+ messageId: editParams.messageId,
117
+ });
118
+ return (0, core_1.jsonResult)({
119
+ ok: true,
120
+ edited: true,
121
+ accountId: editAccountId,
122
+ chatId: editParams.target,
123
+ messageId: editParams.messageId,
124
+ });
125
+ }
65
126
  // Core normalizes whichever of these it filled in to a local path (see
66
127
  // `mediaSourceParams` above); `mediaUrl` stays a URL, which GramJS
67
128
  // accepts as well.
package/dist/actions.js CHANGED
@@ -30,6 +30,14 @@ const ACTION_ALIASES = {
30
30
  list: "read",
31
31
  react: "react",
32
32
  joins: "joins",
33
+ // Rewriting a message already sent (2.29.0). Core has always known the name
34
+ // — it is in `CHANNEL_MESSAGE_ACTION_NAMES` — so the call reached this
35
+ // channel and was refused as unsupported, and a wrong answer could only be
36
+ // followed by a second message correcting it.
37
+ edit: "edit",
38
+ editMessage: "edit",
39
+ "edit-message": "edit",
40
+ update: "edit",
33
41
  "upload-file": "upload-file",
34
42
  sendAttachment: "upload-file",
35
43
  "fetch-media": "fetch-media",
@@ -108,6 +116,11 @@ function canonicalAction(action) {
108
116
  * call fell through to the current chat and answered about the wrong one.
109
117
  * `readChatTargetParam` is the single list of accepted spellings now.
110
118
  *
119
+ * `edit` (2.29.0) is the third shape: core resolves it as a *message* target,
120
+ * so both the chat and `messageId` arrive named. It needs no synonym — the
121
+ * name core uses is the name this channel uses — and it is listed here only so
122
+ * the suite asserts core still knows it.
123
+ *
111
124
  * These spellings are derived from `ACTION_ALIASES` rather than kept beside
112
125
  * it; that core actually knows each of them is asserted against the installed
113
126
  * core in `core-action-synonyms.test.ts`.
@@ -115,6 +128,7 @@ function canonicalAction(action) {
115
128
  const CORE_VOCABULARY_SPELLINGS = [
116
129
  "thread-list", "channel-list", "channel-info", "member-info", "download-file",
117
130
  "channel-create", "addParticipant", "kick", "role-add", "role-remove",
131
+ "edit",
118
132
  ];
119
133
  exports.CORE_ACTION_SYNONYMS = Object.fromEntries(CORE_VOCABULARY_SPELLINGS.map((name) => [name, ACTION_ALIASES[name]]));
120
134
  /** Canonical actions that go through the chat-management gate. */
package/dist/channel.js CHANGED
@@ -454,6 +454,9 @@ const createChannelPlugin = (runtimes, pluginRuntime) => {
454
454
  // one. `joins` likewise.
455
455
  actions: [
456
456
  "send", "read", "react", "upload-file",
457
+ // Rewriting a message this account already sent (2.29.0). Core
458
+ // knows the name, so it needs no synonym — unlike the reads below.
459
+ "edit",
457
460
  // Reading an attachment that is already in a chat. `read` reports
458
461
  // that a photo exists; this is what turns it into something the
459
462
  // agent can look at or pass on.
package/dist/edits.js ADDED
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ /**
3
+ * Editing a message already sent — the `edit` action of OpenClaw's message
4
+ * tool.
5
+ *
6
+ * Telegram lets an account rewrite its own message in place, and until 2.29.0
7
+ * this channel had no way to ask for it: a wrong answer could only be followed
8
+ * by a second message correcting the first, which leaves both standing in the
9
+ * chat. `edit` is in core's `CHANNEL_MESSAGE_ACTION_NAMES`, so the name was
10
+ * reachable from the tool the whole time and simply arrived at a channel that
11
+ * refused it.
12
+ *
13
+ * Parsing lives here, apart from the network, so the argument handling can be
14
+ * tested without a Telegram connection — the same split as `reactions.ts`.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.parseEditParams = parseEditParams;
18
+ const helpers_1 = require("./helpers");
19
+ const history_1 = require("./history");
20
+ function parseEditParams(params, toolContext) {
21
+ const rawTarget = (0, helpers_1.readChatTargetParam)(params, toolContext);
22
+ const target = typeof rawTarget === "string" ? rawTarget.trim() : "";
23
+ if (!target) {
24
+ throw new Error("clawgram: edit requires a chatId");
25
+ }
26
+ // No fall-back to `currentMessageId`, and that is the difference from
27
+ // `react`. The current message is the one being answered — someone else's.
28
+ // Reacting to it is the normal case; editing it is never a thing this
29
+ // account may do, so a missing id is a caller mistake and not an invitation
30
+ // to guess. Telegram would refuse it, but the refusal would arrive as
31
+ // MESSAGE_AUTHOR_REQUIRED on a call nobody meant to make.
32
+ const messageId = (0, history_1.parseMessageId)(params.messageId ?? params.msgId ?? params.message_id, "edit messageId");
33
+ if (messageId === undefined) {
34
+ throw new Error("clawgram: edit requires a messageId — the id of the message to rewrite");
35
+ }
36
+ // `readMessageText` reads `message` then `text`, exactly as `send` does, so
37
+ // the same call shape works for both actions.
38
+ const text = (0, helpers_1.readMessageText)(params).replaceAll("\\n", "\n");
39
+ if (!text.trim()) {
40
+ // Telegram has no "edit to nothing": an empty edit is refused server-side,
41
+ // and a caller who meant to remove the message asked for the wrong action.
42
+ // Saying so here beats an MTProto error about message content.
43
+ throw new Error("clawgram: edit requires the new text — an empty edit is not a deletion");
44
+ }
45
+ return { target, messageId, text };
46
+ }
@@ -427,6 +427,36 @@ class GramJsClientManager {
427
427
  ...replyParams,
428
428
  });
429
429
  }
430
+ /**
431
+ * Rewrites a message this account already sent.
432
+ *
433
+ * Rendering mirrors `sendText` deliberately: an edit that parsed its text
434
+ * differently from the send would change the formatting of a message nobody
435
+ * asked to reformat — markdown shipped as literal asterisks is exactly the
436
+ * failure that made `sendText` render HTML itself.
437
+ *
438
+ * No chunking here, unlike `sendText`. A send too long for Telegram becomes
439
+ * several messages; an edit cannot — there is one message to rewrite. Over
440
+ * the limit the call fails, and that is the honest outcome.
441
+ *
442
+ * Telegram's own refusals are left to reach the caller as they are:
443
+ * MESSAGE_AUTHOR_REQUIRED (not this account's message),
444
+ * MESSAGE_EDIT_TIME_EXPIRED (past the window Telegram allows) and
445
+ * MESSAGE_NOT_MODIFIED (the new text equals the old one). None of them is
446
+ * retryable, and dressing them up would hide which one happened.
447
+ */
448
+ async editText(args) {
449
+ const resolved = await this.resolvePeer(args.target);
450
+ return this.client.editMessage(resolved.peer, {
451
+ message: args.messageId,
452
+ text: args.parseMode === "html" ? (0, html_render_1.renderTelegramHtml)(args.text) : args.text,
453
+ ...(args.parseMode === "none"
454
+ ? { parseMode: false }
455
+ : args.parseMode
456
+ ? { parseMode: args.parseMode === "markdown" ? "md" : "html" }
457
+ : {}),
458
+ });
459
+ }
430
460
  /**
431
461
  * Reads what a chat is: title, type, member count, description, pinned
432
462
  * message.
@@ -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.28.1",
5
+ "version": "2.29.0",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
@@ -468,7 +468,7 @@
468
468
  "items": {
469
469
  "type": "string"
470
470
  },
471
- "description": "Chat ids the account may SEND to (2.18.0): send, upload-file, react and core's delivery path. Same shape as readChats — absent means no restriction, [] denies everything, [\"*\"] allows every chat. A phone number is refused in every configuration, wildcard included: messaging a raw number starts a conversation with someone who never interacted with the account. Without this list an injected turn can message strangers from the owner's personal account or carry a work chat's content into a DM one send at a time."
471
+ "description": "Chat ids the account may SEND to (2.18.0): send, upload-file, react, edit and core's delivery path. Same shape as readChats — absent means no restriction, [] denies everything, [\"*\"] allows every chat. A phone number is refused in every configuration, wildcard included: messaging a raw number starts a conversation with someone who never interacted with the account. Without this list an injected turn can message strangers from the owner's personal account or carry a work chat's content into a DM one send at a time."
472
472
  },
473
473
  "manageChats": {
474
474
  "type": "array",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawgram",
3
- "version": "2.28.1",
3
+ "version": "2.29.0",
4
4
  "description": "Clawgram — personal Telegram (MTProto userbot) channel for OpenClaw. Your AI assistant reads and responds as you.",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {