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 +38 -9
- package/dist/account-registry.js +33 -0
- package/dist/account-scopes.js +76 -0
- package/dist/action-context.js +2 -0
- package/dist/actions-manage.js +198 -0
- package/dist/actions-read.js +394 -0
- package/dist/actions-send.js +302 -0
- package/dist/channel.js +37 -975
- package/dist/gramjs-client.js +7 -25
- package/dist/helpers.js +5 -2
- package/dist/history.js +52 -5
- package/dist/inbound-pipeline.js +68 -106
- package/dist/manage.js +1 -6
- package/dist/media.js +22 -0
- package/dist/outbound.js +22 -24
- package/dist/send-scope.js +13 -26
- package/dist/system-notice.js +0 -20
- package/npm-shrinkwrap.json +2 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.handleReadAction = handleReadAction;
|
|
7
|
+
const core_1 = require("openclaw/plugin-sdk/core");
|
|
8
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const account_scopes_1 = require("./account-scopes");
|
|
11
|
+
const attachments_1 = require("./attachments");
|
|
12
|
+
const chat_info_1 = require("./chat-info");
|
|
13
|
+
const dialogs_1 = require("./dialogs");
|
|
14
|
+
const fetch_media_1 = require("./fetch-media");
|
|
15
|
+
const history_1 = require("./history");
|
|
16
|
+
const joins_1 = require("./joins");
|
|
17
|
+
const media_1 = require("./media");
|
|
18
|
+
const state_dir_1 = require("./state-dir");
|
|
19
|
+
const topics_1 = require("./topics");
|
|
20
|
+
/**
|
|
21
|
+
* The read-shaped actions: `read`, `fetch-media`, `participants`, `topics`,
|
|
22
|
+
* `dialogs`, `joins`, `chatInfo`.
|
|
23
|
+
*
|
|
24
|
+
* Cut out of `handleAction` in `channel.ts` unchanged — same gates, same log
|
|
25
|
+
* lines, same answers; the probe in `scripts/verify/action-probe.cjs` is the
|
|
26
|
+
* proof (audit B5-13, part 3). Answers `undefined` for any other action.
|
|
27
|
+
*/
|
|
28
|
+
const actionLog = (0, core_1.createSubsystemLogger)("channels/clawgram");
|
|
29
|
+
// Час, а не сутки: `fetch-media` существует ради «прочитать и переслать», и
|
|
30
|
+
// файл нужен ровно на время хода. Сутки означали сутки чужой личной переписки
|
|
31
|
+
// на диске (A5-13).
|
|
32
|
+
const FETCHED_MEDIA_TTL_MS = 60 * 60 * 1000;
|
|
33
|
+
async function handleReadAction(ctx) {
|
|
34
|
+
const { canonical, params, cfg, accountId, toolContext, pluginRuntime, resolveRuntimeAccountId, requireRuntimeFor } = ctx;
|
|
35
|
+
// `read` is what OpenClaw core dispatches (`openclaw message read`,
|
|
36
|
+
// MCP `messages_read`); `list` resolves to it too.
|
|
37
|
+
if (canonical === "read") {
|
|
38
|
+
const listParams = (0, history_1.parseListMessagesParams)(params);
|
|
39
|
+
const listAccountId = resolveRuntimeAccountId(cfg, accountId);
|
|
40
|
+
if (!listAccountId) {
|
|
41
|
+
throw new Error("clawgram: no configured account found");
|
|
42
|
+
}
|
|
43
|
+
// Reading is not a side effect, so a dry run still answers — reporting
|
|
44
|
+
// an empty window would look like a quiet chat rather than a no-op.
|
|
45
|
+
if (!(0, history_1.isChatReadable)(listParams.target, (0, account_scopes_1.resolveAccountReadChats)(cfg, listAccountId))) {
|
|
46
|
+
actionLog.warn("clawgram list refused: chat outside read scope", {
|
|
47
|
+
accountId: listAccountId,
|
|
48
|
+
target: listParams.target,
|
|
49
|
+
});
|
|
50
|
+
throw new Error(`clawgram: not-allowed-chat ${listParams.target}`);
|
|
51
|
+
}
|
|
52
|
+
const listGram = requireRuntimeFor(listAccountId);
|
|
53
|
+
const history = await listGram.listMessages(listParams);
|
|
54
|
+
// Metadata only. Message text is the user's correspondence and has no
|
|
55
|
+
// business in a log that is read while debugging something else.
|
|
56
|
+
actionLog.info("clawgram handleAction list completed", {
|
|
57
|
+
accountId: listAccountId,
|
|
58
|
+
target: listParams.target,
|
|
59
|
+
limit: listParams.limit,
|
|
60
|
+
since: listParams.since ?? null,
|
|
61
|
+
until: listParams.until ?? null,
|
|
62
|
+
returned: history.messages.length,
|
|
63
|
+
truncated: history.truncated,
|
|
64
|
+
});
|
|
65
|
+
return (0, core_1.jsonResult)({
|
|
66
|
+
ok: true,
|
|
67
|
+
accountId: listAccountId,
|
|
68
|
+
chatId: history.chatId ?? listParams.target,
|
|
69
|
+
count: history.messages.length,
|
|
70
|
+
truncated: history.truncated,
|
|
71
|
+
messages: history.messages,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// The attachment on a message that is already in a chat.
|
|
75
|
+
//
|
|
76
|
+
// `read` says a photo exists; it does not fetch it, and the inbound
|
|
77
|
+
// path only ever reads what arrives while the agent is being addressed.
|
|
78
|
+
// Everything else — a screenshot posted an hour ago, a diagram in a
|
|
79
|
+
// chat the agent reads but was not tagged in — was visible to the
|
|
80
|
+
// channel and unreachable to the agent. Same `readChats` scope as
|
|
81
|
+
// history: this must not become a way to pull bytes out of a chat the
|
|
82
|
+
// account was never allowed to read.
|
|
83
|
+
if (canonical === "fetch-media") {
|
|
84
|
+
const fetchParams = (0, fetch_media_1.parseFetchMediaParams)(params);
|
|
85
|
+
const fetchAccountId = resolveRuntimeAccountId(cfg, accountId);
|
|
86
|
+
if (!fetchAccountId) {
|
|
87
|
+
throw new Error("clawgram: no configured account found");
|
|
88
|
+
}
|
|
89
|
+
if (!(0, history_1.isChatReadable)(fetchParams.target, (0, account_scopes_1.resolveAccountReadChats)(cfg, fetchAccountId))) {
|
|
90
|
+
actionLog.warn("clawgram fetch-media refused: chat outside read scope", {
|
|
91
|
+
accountId: fetchAccountId,
|
|
92
|
+
target: fetchParams.target,
|
|
93
|
+
});
|
|
94
|
+
throw new Error(`clawgram: not-allowed-chat ${fetchParams.target}`);
|
|
95
|
+
}
|
|
96
|
+
const fetchGram = requireRuntimeFor(fetchAccountId);
|
|
97
|
+
// Fetching is a read: a dry run answers for real, the same way `read`
|
|
98
|
+
// does. Nothing leaves the machine — the file lands in a temp
|
|
99
|
+
// directory this channel prunes — so a rehearsal that reported
|
|
100
|
+
// "would fetch" would only teach the agent to ask twice.
|
|
101
|
+
const found = await fetchGram.getMessageById(fetchParams.target, fetchParams.messageId);
|
|
102
|
+
const fetchChatId = found.chatId ?? fetchParams.target;
|
|
103
|
+
if (!found.message) {
|
|
104
|
+
actionLog.info("clawgram fetch-media found no message", {
|
|
105
|
+
accountId: fetchAccountId,
|
|
106
|
+
chatId: fetchChatId,
|
|
107
|
+
messageId: fetchParams.messageId,
|
|
108
|
+
});
|
|
109
|
+
return (0, core_1.jsonResult)({
|
|
110
|
+
ok: false,
|
|
111
|
+
accountId: fetchAccountId,
|
|
112
|
+
chatId: fetchChatId,
|
|
113
|
+
messageId: String(fetchParams.messageId),
|
|
114
|
+
error: "message-not-found",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
// `read` throws the file away, so it gets a directory of its own —
|
|
118
|
+
// the shared directory is keyed by chat and message, and deleting
|
|
119
|
+
// that path would pull the file out from under an earlier `both`
|
|
120
|
+
// fetch of the same message that handed the caller a path.
|
|
121
|
+
// Не общий /tmp: там файлы видит каждый локальный пользователь, а на
|
|
122
|
+
// этом хосте живёт ещё и раннер деплоя. Каталог состояния OpenClaw
|
|
123
|
+
// принадлежит агенту; если он не задан, остаётся /tmp — но права
|
|
124
|
+
// 0700/0600 ставятся в любом случае (A5-13).
|
|
125
|
+
// Каталог состояния принадлежит агенту; при явно заданном
|
|
126
|
+
// OPENCLAW_STATE_DIR вложения не покидают его.
|
|
127
|
+
const mediaRoot = process.env.OPENCLAW_STATE_DIR?.trim()
|
|
128
|
+
? node_path_1.default.join((0, state_dir_1.resolveStateDir)(), "tmp")
|
|
129
|
+
: node_os_1.default.tmpdir();
|
|
130
|
+
const sharedFetchDir = node_path_1.default.join(mediaRoot, "clawgram-fetched");
|
|
131
|
+
let fetchDir = sharedFetchDir;
|
|
132
|
+
if (fetchParams.mode === "read") {
|
|
133
|
+
const { mkdtemp, mkdir } = await import("node:fs/promises");
|
|
134
|
+
await mkdir(mediaRoot, { recursive: true, mode: 0o700 });
|
|
135
|
+
fetchDir = await mkdtemp(node_path_1.default.join(mediaRoot, "clawgram-media-"));
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
await (0, media_1.pruneFetchedMedia)(sharedFetchDir, FETCHED_MEDIA_TTL_MS, Date.now());
|
|
139
|
+
}
|
|
140
|
+
const downloaded = await (0, media_1.downloadMessageMediaToFile)({
|
|
141
|
+
client: fetchGram.getClient(),
|
|
142
|
+
message: found.message,
|
|
143
|
+
maxBytes: attachments_1.INBOUND_MEDIA_MAX_BYTES,
|
|
144
|
+
dir: fetchDir,
|
|
145
|
+
fileNameFor: ({ media, extension }) => (0, fetch_media_1.fetchedMediaFileName)({
|
|
146
|
+
chatId: fetchChatId,
|
|
147
|
+
messageId: fetchParams.messageId,
|
|
148
|
+
extension,
|
|
149
|
+
fileName: media.fileName,
|
|
150
|
+
}),
|
|
151
|
+
});
|
|
152
|
+
if (!downloaded) {
|
|
153
|
+
// Three different nothings, and the agent has to be able to tell
|
|
154
|
+
// them apart: a message with no attachment, an attachment this
|
|
155
|
+
// channel does not read (a video, a spreadsheet), and one too
|
|
156
|
+
// large to be worth the transfer. Saying "could not fetch" to all
|
|
157
|
+
// three is how "she ignored the picture" starts.
|
|
158
|
+
const described = (0, media_1.describeMedia)(found.message?.media);
|
|
159
|
+
const tooLarge = typeof described?.size === "number" && described.size > attachments_1.INBOUND_MEDIA_MAX_BYTES;
|
|
160
|
+
const error = !described
|
|
161
|
+
? "no-media"
|
|
162
|
+
: tooLarge
|
|
163
|
+
? "media-too-large"
|
|
164
|
+
: "unsupported-media";
|
|
165
|
+
actionLog.info("clawgram fetch-media returned nothing", {
|
|
166
|
+
accountId: fetchAccountId,
|
|
167
|
+
chatId: fetchChatId,
|
|
168
|
+
messageId: fetchParams.messageId,
|
|
169
|
+
kind: described?.kind ?? null,
|
|
170
|
+
error,
|
|
171
|
+
});
|
|
172
|
+
return (0, core_1.jsonResult)({
|
|
173
|
+
ok: false,
|
|
174
|
+
accountId: fetchAccountId,
|
|
175
|
+
chatId: fetchChatId,
|
|
176
|
+
messageId: String(fetchParams.messageId),
|
|
177
|
+
media: described ?? null,
|
|
178
|
+
error,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
let read;
|
|
182
|
+
let readError;
|
|
183
|
+
if (fetchParams.mode !== "file") {
|
|
184
|
+
try {
|
|
185
|
+
read = await (0, attachments_1.understandAttachmentFile)({
|
|
186
|
+
runtime: pluginRuntime,
|
|
187
|
+
cfg,
|
|
188
|
+
filePath: downloaded.path,
|
|
189
|
+
mimeType: downloaded.mimeType,
|
|
190
|
+
understanding: downloaded.understanding,
|
|
191
|
+
});
|
|
192
|
+
if (!read) {
|
|
193
|
+
readError = "read-empty";
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
// The bytes are already here. A failed reading is worth
|
|
198
|
+
// reporting, but it does not undo a successful fetch: the file
|
|
199
|
+
// still exists and can still be forwarded.
|
|
200
|
+
readError = String(err);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// `read` mode is the inbound contract — the words, not the file — so
|
|
204
|
+
// the bytes go away with the answer. Any other mode keeps them:
|
|
205
|
+
// that is the whole point of asking for a path.
|
|
206
|
+
if (fetchParams.mode === "read") {
|
|
207
|
+
try {
|
|
208
|
+
const { rm } = await import("node:fs/promises");
|
|
209
|
+
await rm(fetchDir, { recursive: true, force: true });
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// A file left behind is pruned within a day; failing the call
|
|
213
|
+
// over it would throw away a reading that already succeeded.
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
actionLog.info("clawgram fetch-media completed", {
|
|
217
|
+
accountId: fetchAccountId,
|
|
218
|
+
chatId: fetchChatId,
|
|
219
|
+
messageId: fetchParams.messageId,
|
|
220
|
+
mode: fetchParams.mode,
|
|
221
|
+
kind: downloaded.media.kind,
|
|
222
|
+
understanding: downloaded.understanding,
|
|
223
|
+
characters: read?.length ?? 0,
|
|
224
|
+
readError: readError ?? null,
|
|
225
|
+
});
|
|
226
|
+
return (0, core_1.jsonResult)({
|
|
227
|
+
ok: true,
|
|
228
|
+
accountId: fetchAccountId,
|
|
229
|
+
chatId: fetchChatId,
|
|
230
|
+
messageId: String(fetchParams.messageId),
|
|
231
|
+
mode: fetchParams.mode,
|
|
232
|
+
media: downloaded.media,
|
|
233
|
+
understanding: downloaded.understanding,
|
|
234
|
+
filePath: fetchParams.mode === "read" ? undefined : downloaded.path,
|
|
235
|
+
text: read,
|
|
236
|
+
readError,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* The scaffold every chat-shaped read shares.
|
|
241
|
+
*
|
|
242
|
+
* `participants`, `topics`, `dialogs`, `joins` and `chatInfo` each
|
|
243
|
+
* spelled out the same sequence: parse, resolve the account, check a
|
|
244
|
+
* scope, fetch the runtime, call it, log counts, answer. Roughly
|
|
245
|
+
* forty lines apiece, differing in four places — which is how a new
|
|
246
|
+
* action came to cost sixty lines of scaffold and how the two gates
|
|
247
|
+
* drifted apart (finding A6-11).
|
|
248
|
+
*
|
|
249
|
+
* The gate follows from the shape rather than being restated: an
|
|
250
|
+
* action that names a chat is gated by `readChats`, `dialogs` has its
|
|
251
|
+
* own discovery gate precisely because its point is to find chats
|
|
252
|
+
* that are not in scope yet, and `joins` has none — the journal only
|
|
253
|
+
* ever holds chats this account was put into.
|
|
254
|
+
*
|
|
255
|
+
* The runtime is a getter, not a value: `joins` reads a file and must
|
|
256
|
+
* not fail merely because no runtime is connected.
|
|
257
|
+
*/
|
|
258
|
+
const runRead = async (spec) => {
|
|
259
|
+
const parsed = spec.parse();
|
|
260
|
+
const readAccountId = resolveRuntimeAccountId(cfg, accountId);
|
|
261
|
+
if (!readAccountId) {
|
|
262
|
+
throw new Error("clawgram: no configured account found");
|
|
263
|
+
}
|
|
264
|
+
const target = spec.target?.(parsed);
|
|
265
|
+
if (target !== undefined) {
|
|
266
|
+
if (!(0, history_1.isChatReadable)(target, (0, account_scopes_1.resolveAccountReadChats)(cfg, readAccountId))) {
|
|
267
|
+
actionLog.warn(`clawgram ${spec.name} refused: chat outside read scope`, {
|
|
268
|
+
accountId: readAccountId,
|
|
269
|
+
target,
|
|
270
|
+
});
|
|
271
|
+
throw new Error(`clawgram: not-allowed-chat ${target}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else if (spec.discovery) {
|
|
275
|
+
if (!(0, dialogs_1.isChatDiscoveryEnabled)((0, account_scopes_1.resolveAccountDiscoverChats)(cfg, readAccountId))) {
|
|
276
|
+
actionLog.warn(`clawgram ${spec.name} refused: chat-discovery is not enabled`, {
|
|
277
|
+
accountId: readAccountId,
|
|
278
|
+
});
|
|
279
|
+
throw new Error("clawgram: chat-discovery is not enabled");
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const gram = () => requireRuntimeFor(readAccountId);
|
|
283
|
+
const result = await spec.run({ parsed, accountId: readAccountId, gram });
|
|
284
|
+
actionLog.info(`clawgram handleAction ${spec.name} completed`, {
|
|
285
|
+
accountId: readAccountId,
|
|
286
|
+
...spec.after(parsed, result),
|
|
287
|
+
});
|
|
288
|
+
return (0, core_1.jsonResult)({ ok: true, accountId: readAccountId, ...spec.result(parsed, result) });
|
|
289
|
+
};
|
|
290
|
+
// Membership is a read, so the same `readChats` scope that gates history
|
|
291
|
+
// gates it too: this cannot become a way to enumerate chats the account
|
|
292
|
+
// was never allowed to read.
|
|
293
|
+
if (canonical === "participants") {
|
|
294
|
+
return await runRead({
|
|
295
|
+
name: "participants",
|
|
296
|
+
parse: () => (0, history_1.parseListParticipantsParams)(params),
|
|
297
|
+
target: (p) => p.target,
|
|
298
|
+
run: ({ parsed, gram }) => gram().listParticipants(parsed),
|
|
299
|
+
// Counts only. Member ids are personal data and have no business in
|
|
300
|
+
// a log that is read while debugging something else.
|
|
301
|
+
after: (p, m) => ({
|
|
302
|
+
target: p.target,
|
|
303
|
+
limit: p.limit,
|
|
304
|
+
returned: m.participants.length,
|
|
305
|
+
truncated: m.truncated,
|
|
306
|
+
}),
|
|
307
|
+
result: (p, m) => ({
|
|
308
|
+
chatId: m.chatId ?? p.target,
|
|
309
|
+
count: m.participants.length,
|
|
310
|
+
truncated: m.truncated,
|
|
311
|
+
participants: m.participants,
|
|
312
|
+
}),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
// Topic names. A forum chat is addressed by topic id, and until now an
|
|
316
|
+
// id could only be lifted off an inbound message — so a topic nobody had
|
|
317
|
+
// written in yet was unreachable, and one named in words was unfindable.
|
|
318
|
+
// Titles say what a chat is working on, so the read scope gates them.
|
|
319
|
+
if (canonical === "topics") {
|
|
320
|
+
return await runRead({
|
|
321
|
+
name: "topics",
|
|
322
|
+
parse: () => (0, topics_1.parseTopicsParams)(params),
|
|
323
|
+
target: (p) => p.target,
|
|
324
|
+
run: ({ parsed, gram }) => gram().listTopics(parsed),
|
|
325
|
+
after: (p, f) => ({
|
|
326
|
+
target: p.target,
|
|
327
|
+
limit: p.limit,
|
|
328
|
+
returned: f.topics.length,
|
|
329
|
+
truncated: f.truncated,
|
|
330
|
+
}),
|
|
331
|
+
result: (p, f) => ({
|
|
332
|
+
chatId: f.chatId ?? p.target,
|
|
333
|
+
count: f.topics.length,
|
|
334
|
+
truncated: f.truncated,
|
|
335
|
+
topics: f.topics,
|
|
336
|
+
}),
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
// Which chats this account is in. Not gated by `readChats` — the whole
|
|
340
|
+
// point is to find chats that are not in it yet — so it has a gate of
|
|
341
|
+
// its own, is metadata only, and never reports direct chats.
|
|
342
|
+
if (canonical === "dialogs") {
|
|
343
|
+
return await runRead({
|
|
344
|
+
name: "dialogs",
|
|
345
|
+
parse: () => (0, dialogs_1.parseDialogsParams)(params),
|
|
346
|
+
discovery: true,
|
|
347
|
+
run: ({ parsed, gram }) => gram().listDialogs(parsed),
|
|
348
|
+
// Counts only: which chats a person's account sits in is exactly
|
|
349
|
+
// the kind of thing that should not be sitting in a log.
|
|
350
|
+
after: (p, f) => ({ limit: p.limit, returned: f.dialogs.length, truncated: f.truncated }),
|
|
351
|
+
result: (_p, f) => ({ count: f.dialogs.length, truncated: f.truncated, dialogs: f.dialogs }),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
// Where this account was recently added, and by whom. Reading the journal
|
|
355
|
+
// has no scope check of its own: it only ever contains chats this account
|
|
356
|
+
// was put into, which is exactly what the caller is allowed to learn.
|
|
357
|
+
if (canonical === "joins") {
|
|
358
|
+
return await runRead({
|
|
359
|
+
name: "joins",
|
|
360
|
+
parse: () => (0, joins_1.parseJoinsParams)(params),
|
|
361
|
+
// No runtime: this reads a file, and must answer with none connected.
|
|
362
|
+
run: async ({ parsed, accountId: joinsAccountId }) => (0, joins_1.selectJoinRecords)((0, joins_1.readJoinRecords)((0, joins_1.resolveJoinsJournalPath)(cfg?.channels?.["clawgram"]?.accounts?.[joinsAccountId], joinsAccountId)), parsed),
|
|
363
|
+
after: (p, selected) => ({
|
|
364
|
+
since: p.since ?? null,
|
|
365
|
+
limit: p.limit,
|
|
366
|
+
returned: selected.length,
|
|
367
|
+
}),
|
|
368
|
+
result: (_p, selected) => ({ count: selected.length, joins: selected }),
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
// Describing a chat is a read, so the same `readChats` scope that gates
|
|
372
|
+
// history gates it too — this must not become a way to learn the title
|
|
373
|
+
// and size of a chat the account was never allowed to read.
|
|
374
|
+
if (canonical === "chatInfo") {
|
|
375
|
+
return await runRead({
|
|
376
|
+
name: "chatInfo",
|
|
377
|
+
parse: () => (0, chat_info_1.parseChatInfoParams)(params, toolContext),
|
|
378
|
+
target: (p) => p.target,
|
|
379
|
+
run: async ({ parsed, gram }) => {
|
|
380
|
+
const { entity, full } = await gram().getChatInfo(parsed.target);
|
|
381
|
+
return (0, chat_info_1.describeChat)(entity, full);
|
|
382
|
+
},
|
|
383
|
+
// Type and size only. The title of a private chat is as personal as
|
|
384
|
+
// its contents and has no business in a debugging log.
|
|
385
|
+
after: (_p, info) => ({
|
|
386
|
+
type: info.type,
|
|
387
|
+
memberCount: info.memberCount ?? null,
|
|
388
|
+
isForum: info.isForum ?? null,
|
|
389
|
+
}),
|
|
390
|
+
result: (p, info) => ({ chat: { ...info, chatId: info.chatId ?? p.target } }),
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|