clawgram 2.0.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/LICENSE +22 -0
- package/README.md +589 -0
- package/dist/channel.js +1233 -0
- package/dist/clawgram-cli.js +17 -0
- package/dist/cli-core.js +236 -0
- package/dist/cli.js +25 -0
- package/dist/constants.js +7 -0
- package/dist/gramjs-client.js +437 -0
- package/dist/group-reply-address.js +79 -0
- package/dist/group-visible-reply-guard.js +48 -0
- package/dist/helpers.js +495 -0
- package/dist/history.js +286 -0
- package/dist/index.js +20 -0
- package/dist/joins.js +147 -0
- package/dist/normalize.js +125 -0
- package/dist/proxy-config.js +106 -0
- package/dist/types.js +2 -0
- package/dist/update-config.js +381 -0
- package/openclaw.plugin.json +253 -0
- package/package.json +68 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GramJsClientManager = void 0;
|
|
4
|
+
const telegram_1 = require("telegram");
|
|
5
|
+
const sessions_1 = require("telegram/sessions");
|
|
6
|
+
const proxy_config_1 = require("./proxy-config");
|
|
7
|
+
const history_1 = require("./history");
|
|
8
|
+
function toStringId(value) {
|
|
9
|
+
if (value === null || value === undefined)
|
|
10
|
+
return undefined;
|
|
11
|
+
try {
|
|
12
|
+
return String(value);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function inferChatTypeFromRaw(raw) {
|
|
19
|
+
if (raw.startsWith("-100"))
|
|
20
|
+
return "channel";
|
|
21
|
+
if (raw.startsWith("-"))
|
|
22
|
+
return "group";
|
|
23
|
+
return "direct";
|
|
24
|
+
}
|
|
25
|
+
function getChatIdFromPeer(peer, fallback) {
|
|
26
|
+
const userId = toStringId(peer?.userId);
|
|
27
|
+
if (userId)
|
|
28
|
+
return userId;
|
|
29
|
+
const chatId = toStringId(peer?.chatId);
|
|
30
|
+
if (chatId)
|
|
31
|
+
return `-${chatId.replace(/^-/, "")}`;
|
|
32
|
+
const channelId = toStringId(peer?.channelId);
|
|
33
|
+
if (channelId)
|
|
34
|
+
return `-100${channelId.replace(/^-100|-/, "")}`;
|
|
35
|
+
return fallback;
|
|
36
|
+
}
|
|
37
|
+
function toSafeInteger(value) {
|
|
38
|
+
if (!/^-?\d+$/.test(value)) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const parsed = Number(value);
|
|
42
|
+
return Number.isSafeInteger(parsed) ? parsed : undefined;
|
|
43
|
+
}
|
|
44
|
+
function uniqueCandidates(values) {
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
const result = [];
|
|
47
|
+
for (const value of values) {
|
|
48
|
+
const key = typeof value === "object" ? String(value) : `${typeof value}:${String(value)}`;
|
|
49
|
+
if (seen.has(key)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
seen.add(key);
|
|
53
|
+
result.push(value);
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
function parseTargetWithThread(rawTarget) {
|
|
58
|
+
const raw = rawTarget.trim();
|
|
59
|
+
const topicMatch = /^(.+?):topic:(\d+)$/.exec(raw);
|
|
60
|
+
if (topicMatch) {
|
|
61
|
+
return {
|
|
62
|
+
raw,
|
|
63
|
+
chatId: topicMatch[1],
|
|
64
|
+
messageThreadId: Number.parseInt(topicMatch[2], 10),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const colonMatch = /^(.+):(\d+)$/.exec(raw);
|
|
68
|
+
if (colonMatch && /^-?\d+$/.test(colonMatch[1])) {
|
|
69
|
+
return {
|
|
70
|
+
raw,
|
|
71
|
+
chatId: colonMatch[1],
|
|
72
|
+
messageThreadId: Number.parseInt(colonMatch[2], 10),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
raw,
|
|
77
|
+
chatId: raw,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function buildForumReplyParams(messageThreadId, replyToMessageId) {
|
|
81
|
+
const normalizedThreadId = typeof messageThreadId === "number" && Number.isFinite(messageThreadId)
|
|
82
|
+
? Math.trunc(messageThreadId)
|
|
83
|
+
: undefined;
|
|
84
|
+
const normalizedReplyToId = typeof replyToMessageId === "number" && Number.isFinite(replyToMessageId)
|
|
85
|
+
? Math.trunc(replyToMessageId)
|
|
86
|
+
: undefined;
|
|
87
|
+
if (!normalizedThreadId || normalizedThreadId <= 1) {
|
|
88
|
+
return normalizedReplyToId ? { replyTo: normalizedReplyToId } : {};
|
|
89
|
+
}
|
|
90
|
+
if (!normalizedReplyToId) {
|
|
91
|
+
return {
|
|
92
|
+
replyTo: normalizedThreadId,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (normalizedReplyToId === normalizedThreadId) {
|
|
96
|
+
return {
|
|
97
|
+
replyTo: normalizedReplyToId,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
replyTo: normalizedReplyToId,
|
|
102
|
+
topMsgId: normalizedThreadId,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function buildPeerCandidates(raw, kind) {
|
|
106
|
+
const candidates = [raw];
|
|
107
|
+
const numeric = toSafeInteger(raw);
|
|
108
|
+
if (numeric !== undefined) {
|
|
109
|
+
candidates.push(numeric);
|
|
110
|
+
}
|
|
111
|
+
if ((kind === "group" || kind === "channel") && /^\d+$/.test(raw)) {
|
|
112
|
+
const supergroupId = `-100${raw}`;
|
|
113
|
+
const basicGroupId = `-${raw}`;
|
|
114
|
+
candidates.push(supergroupId, toSafeInteger(supergroupId), basicGroupId, toSafeInteger(basicGroupId));
|
|
115
|
+
}
|
|
116
|
+
return uniqueCandidates(candidates.filter((value) => value !== undefined));
|
|
117
|
+
}
|
|
118
|
+
function collectDialogKeys(dialog) {
|
|
119
|
+
const keys = new Set();
|
|
120
|
+
const add = (value) => {
|
|
121
|
+
const id = toStringId(value);
|
|
122
|
+
if (id) {
|
|
123
|
+
keys.add(id);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
add(dialog?.id);
|
|
127
|
+
add(dialog?.inputEntity);
|
|
128
|
+
add(dialog?.entity?.id);
|
|
129
|
+
add(getChatIdFromPeer(dialog?.inputEntity));
|
|
130
|
+
add(getChatIdFromPeer(dialog?.entity));
|
|
131
|
+
const inputChatId = toStringId(dialog?.inputEntity?.chatId);
|
|
132
|
+
if (inputChatId) {
|
|
133
|
+
keys.add(inputChatId);
|
|
134
|
+
keys.add(`-${inputChatId.replace(/^-/, "")}`);
|
|
135
|
+
}
|
|
136
|
+
const inputChannelId = toStringId(dialog?.inputEntity?.channelId);
|
|
137
|
+
if (inputChannelId) {
|
|
138
|
+
keys.add(inputChannelId);
|
|
139
|
+
keys.add(`-100${inputChannelId.replace(/^-100|-/, "")}`);
|
|
140
|
+
}
|
|
141
|
+
return keys;
|
|
142
|
+
}
|
|
143
|
+
function buildTargetKeys(raw, kind) {
|
|
144
|
+
const keys = new Set();
|
|
145
|
+
const add = (value) => {
|
|
146
|
+
const id = toStringId(value);
|
|
147
|
+
if (id) {
|
|
148
|
+
keys.add(id);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
add(raw);
|
|
152
|
+
if ((kind === "group" || kind === "channel") && /^\d+$/.test(raw)) {
|
|
153
|
+
add(`-100${raw}`);
|
|
154
|
+
add(`-${raw}`);
|
|
155
|
+
}
|
|
156
|
+
if (raw.startsWith("-100")) {
|
|
157
|
+
add(raw.replace(/^-100/, ""));
|
|
158
|
+
}
|
|
159
|
+
else if (raw.startsWith("-")) {
|
|
160
|
+
add(raw.replace(/^-/, ""));
|
|
161
|
+
}
|
|
162
|
+
return keys;
|
|
163
|
+
}
|
|
164
|
+
class GramJsClientManager {
|
|
165
|
+
config;
|
|
166
|
+
client;
|
|
167
|
+
proxy;
|
|
168
|
+
started = false;
|
|
169
|
+
constructor(config) {
|
|
170
|
+
this.config = config;
|
|
171
|
+
const clientOptions = (0, proxy_config_1.buildTelegramClientOptions)(config.proxy);
|
|
172
|
+
this.proxy = clientOptions.proxy;
|
|
173
|
+
this.client = new telegram_1.TelegramClient(new sessions_1.StringSession(config.sessionString), config.apiId, config.apiHash, clientOptions);
|
|
174
|
+
}
|
|
175
|
+
/** Credential-free proxy summary (`socks4`/`socks5`) for diagnostics. */
|
|
176
|
+
getProxySummary() {
|
|
177
|
+
return (0, proxy_config_1.describeProxy)(this.proxy);
|
|
178
|
+
}
|
|
179
|
+
async start() {
|
|
180
|
+
if (this.started)
|
|
181
|
+
return;
|
|
182
|
+
await this.client.connect();
|
|
183
|
+
const authorized = await this.client.checkAuthorization();
|
|
184
|
+
if (!authorized) {
|
|
185
|
+
throw new Error("GramJS client connected, but session is not authorized.");
|
|
186
|
+
}
|
|
187
|
+
this.started = true;
|
|
188
|
+
}
|
|
189
|
+
async stop() {
|
|
190
|
+
if (!this.started)
|
|
191
|
+
return;
|
|
192
|
+
await this.client.disconnect();
|
|
193
|
+
this.started = false;
|
|
194
|
+
}
|
|
195
|
+
getClient() {
|
|
196
|
+
return this.client;
|
|
197
|
+
}
|
|
198
|
+
async getMe() {
|
|
199
|
+
return this.client.getMe();
|
|
200
|
+
}
|
|
201
|
+
async resolveDialogPeer(raw, kind) {
|
|
202
|
+
const targetKeys = buildTargetKeys(raw, kind);
|
|
203
|
+
const dialogs = await this.client.getDialogs({ limit: 200 }).catch(() => []);
|
|
204
|
+
for (const dialog of dialogs) {
|
|
205
|
+
if (kind === "user" && !dialog?.isUser) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if ((kind === "group" || kind === "channel") && !dialog?.isGroup && !dialog?.isChannel) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const dialogKeys = collectDialogKeys(dialog);
|
|
212
|
+
const matched = [...targetKeys].some((key) => dialogKeys.has(key));
|
|
213
|
+
if (!matched) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const chatId = getChatIdFromPeer(dialog?.inputEntity) ?? toStringId(dialog?.id) ?? raw;
|
|
217
|
+
const chatType = kind === "group"
|
|
218
|
+
? "group"
|
|
219
|
+
: kind === "channel"
|
|
220
|
+
? "channel"
|
|
221
|
+
: dialog?.isGroup
|
|
222
|
+
? "group"
|
|
223
|
+
: dialog?.isChannel
|
|
224
|
+
? "channel"
|
|
225
|
+
: "direct";
|
|
226
|
+
return {
|
|
227
|
+
raw,
|
|
228
|
+
peer: dialog.inputEntity,
|
|
229
|
+
chatId,
|
|
230
|
+
chatType,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
async resolvePeer(rawTarget, options) {
|
|
236
|
+
if (typeof rawTarget !== "string") {
|
|
237
|
+
const entity = await this.client.getInputEntity(rawTarget).catch(() => rawTarget);
|
|
238
|
+
return {
|
|
239
|
+
raw: String(rawTarget ?? ""),
|
|
240
|
+
peer: entity
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const parsedTarget = parseTargetWithThread(rawTarget);
|
|
244
|
+
const raw = parsedTarget.raw;
|
|
245
|
+
const chatLookupTarget = parsedTarget.chatId;
|
|
246
|
+
const kind = options?.kind;
|
|
247
|
+
if (raw === "me" || raw === "self" || raw === "saved") {
|
|
248
|
+
return {
|
|
249
|
+
raw,
|
|
250
|
+
peer: "me",
|
|
251
|
+
chatId: "me",
|
|
252
|
+
messageThreadId: parsedTarget.messageThreadId,
|
|
253
|
+
chatType: "direct"
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
let entity;
|
|
257
|
+
for (const candidate of buildPeerCandidates(chatLookupTarget, kind)) {
|
|
258
|
+
entity = await this.client.getInputEntity(candidate).catch(() => undefined);
|
|
259
|
+
if (entity) {
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (!entity) {
|
|
264
|
+
const dialogResolved = await this.resolveDialogPeer(chatLookupTarget, kind);
|
|
265
|
+
if (dialogResolved) {
|
|
266
|
+
dialogResolved.messageThreadId = parsedTarget.messageThreadId;
|
|
267
|
+
return dialogResolved;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (!entity) {
|
|
271
|
+
entity = await this.client.getInputEntity(chatLookupTarget);
|
|
272
|
+
}
|
|
273
|
+
const chatId = getChatIdFromPeer(entity, chatLookupTarget);
|
|
274
|
+
return {
|
|
275
|
+
raw,
|
|
276
|
+
peer: entity,
|
|
277
|
+
chatId,
|
|
278
|
+
messageThreadId: parsedTarget.messageThreadId,
|
|
279
|
+
chatType: inferChatTypeFromRaw(chatId ?? raw)
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
async sendText(args) {
|
|
283
|
+
const resolved = await this.resolvePeer(args.target, { kind: args.targetKind });
|
|
284
|
+
const messageThreadId = args.messageThreadId ?? resolved.messageThreadId;
|
|
285
|
+
const replyParams = buildForumReplyParams(messageThreadId, args.replyToMessageId);
|
|
286
|
+
return this.client.sendMessage(resolved.peer, {
|
|
287
|
+
message: args.text,
|
|
288
|
+
...replyParams,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Reads a window of chat history.
|
|
293
|
+
*
|
|
294
|
+
* Telegram returns newest first and `offsetDate` means "older than this", so
|
|
295
|
+
* an upper bound is expressed by starting there. The lower bound has no
|
|
296
|
+
* server-side equivalent in this call, so it is applied after the fact — which
|
|
297
|
+
* is also why `limit` is the real guard: a window spanning a quiet week and a
|
|
298
|
+
* window spanning a busy hour cost the same request but not the same context.
|
|
299
|
+
*/
|
|
300
|
+
async listMessages(args) {
|
|
301
|
+
const resolved = await this.resolvePeer(args.target);
|
|
302
|
+
const query = (0, history_1.buildHistoryQuery)(args);
|
|
303
|
+
const fetched = await this.client.getMessages(resolved.peer, query);
|
|
304
|
+
const raw = Array.isArray(fetched) ? fetched : [];
|
|
305
|
+
return {
|
|
306
|
+
chatId: resolved.chatId,
|
|
307
|
+
messages: (0, history_1.collectHistoryWindow)(raw, {
|
|
308
|
+
since: args.since,
|
|
309
|
+
until: args.until,
|
|
310
|
+
fallbackChatId: resolved.chatId,
|
|
311
|
+
}),
|
|
312
|
+
truncated: raw.length >= args.limit,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Chat membership, ids only. The caller needs to answer "do we share a group
|
|
317
|
+
* with this person" — an id answers that and a full profile does not, so
|
|
318
|
+
* names and phone numbers are deliberately left out.
|
|
319
|
+
*/
|
|
320
|
+
async listParticipants(args) {
|
|
321
|
+
const resolved = await this.resolvePeer(args.target);
|
|
322
|
+
const fetched = await this.client.getParticipants(resolved.peer, {
|
|
323
|
+
limit: args.limit,
|
|
324
|
+
});
|
|
325
|
+
const raw = Array.isArray(fetched) ? fetched : [];
|
|
326
|
+
const participants = [];
|
|
327
|
+
for (const entry of raw) {
|
|
328
|
+
const rawId = entry?.id;
|
|
329
|
+
if (rawId === undefined || rawId === null) {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const username = typeof entry?.username === "string" && entry.username.length > 0
|
|
333
|
+
? entry.username
|
|
334
|
+
: undefined;
|
|
335
|
+
const member = {
|
|
336
|
+
userId: String(rawId),
|
|
337
|
+
username,
|
|
338
|
+
isBot: entry?.bot === true,
|
|
339
|
+
};
|
|
340
|
+
// Display names are personal data, so they are opt-in: only the identity
|
|
341
|
+
// linking flow asks for them, and it discards them once a link is made.
|
|
342
|
+
if (args.includeNames) {
|
|
343
|
+
if (typeof entry?.firstName === "string" && entry.firstName.length > 0)
|
|
344
|
+
member.firstName = entry.firstName;
|
|
345
|
+
if (typeof entry?.lastName === "string" && entry.lastName.length > 0)
|
|
346
|
+
member.lastName = entry.lastName;
|
|
347
|
+
}
|
|
348
|
+
participants.push(member);
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
chatId: resolved.chatId,
|
|
352
|
+
participants,
|
|
353
|
+
truncated: raw.length >= args.limit,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
async markRead(target, messageId, options) {
|
|
357
|
+
if (!messageId || !Number.isFinite(messageId)) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const resolved = await this.resolvePeer(target);
|
|
361
|
+
const messageThreadId = options?.messageThreadId ?? resolved.messageThreadId;
|
|
362
|
+
if (messageThreadId && messageThreadId > 1) {
|
|
363
|
+
await this.client.invoke(new telegram_1.Api.messages.ReadDiscussion({
|
|
364
|
+
peer: resolved.peer,
|
|
365
|
+
msgId: messageThreadId,
|
|
366
|
+
readMaxId: messageId,
|
|
367
|
+
})).catch(() => undefined);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
await this.client.markAsRead(resolved.peer, messageId).catch(() => undefined);
|
|
371
|
+
}
|
|
372
|
+
async withTyping(target, fn, options) {
|
|
373
|
+
let peer;
|
|
374
|
+
let readMarked = false;
|
|
375
|
+
let stopped = false;
|
|
376
|
+
let activeTick;
|
|
377
|
+
const sendTyping = async () => {
|
|
378
|
+
if (!peer) {
|
|
379
|
+
peer = (await this.resolvePeer(target)).peer;
|
|
380
|
+
}
|
|
381
|
+
if (stopped) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (!readMarked) {
|
|
385
|
+
readMarked = true;
|
|
386
|
+
await this.markRead(peer, options?.readMessageId, {
|
|
387
|
+
messageThreadId: options?.messageThreadId,
|
|
388
|
+
}).catch(() => undefined);
|
|
389
|
+
}
|
|
390
|
+
if (stopped) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
await this.client.invoke(new telegram_1.Api.messages.SetTyping({
|
|
394
|
+
peer: peer,
|
|
395
|
+
topMsgId: options?.messageThreadId,
|
|
396
|
+
action: new telegram_1.Api.SendMessageTypingAction(),
|
|
397
|
+
}));
|
|
398
|
+
};
|
|
399
|
+
const tick = () => {
|
|
400
|
+
if (stopped || activeTick) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
activeTick = sendTyping()
|
|
404
|
+
.catch(() => undefined)
|
|
405
|
+
.finally(() => {
|
|
406
|
+
activeTick = undefined;
|
|
407
|
+
});
|
|
408
|
+
};
|
|
409
|
+
tick();
|
|
410
|
+
const interval = setInterval(tick, 4000);
|
|
411
|
+
try {
|
|
412
|
+
return await fn();
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
stopped = true;
|
|
416
|
+
clearInterval(interval);
|
|
417
|
+
await activeTick?.catch(() => undefined);
|
|
418
|
+
if (peer) {
|
|
419
|
+
await this.client.invoke(new telegram_1.Api.messages.SetTyping({
|
|
420
|
+
peer: peer,
|
|
421
|
+
action: new telegram_1.Api.SendMessageCancelAction(),
|
|
422
|
+
})).catch(() => undefined);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
async sendMedia(args) {
|
|
427
|
+
const resolved = await this.resolvePeer(args.target);
|
|
428
|
+
const messageThreadId = args.messageThreadId ?? resolved.messageThreadId;
|
|
429
|
+
const replyParams = buildForumReplyParams(messageThreadId, args.replyToMessageId);
|
|
430
|
+
return this.client.sendFile(resolved.peer, {
|
|
431
|
+
file: args.file,
|
|
432
|
+
caption: args.caption,
|
|
433
|
+
...replyParams,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
exports.GramJsClientManager = GramJsClientManager;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.rememberGroupReplyAddress = rememberGroupReplyAddress;
|
|
4
|
+
exports.consumeGroupReplyAddress = consumeGroupReplyAddress;
|
|
5
|
+
exports.buildGroupReplyAddress = buildGroupReplyAddress;
|
|
6
|
+
const helpers_1 = require("./helpers");
|
|
7
|
+
const groupReplyAddresses = new Map();
|
|
8
|
+
const GROUP_REPLY_ADDRESS_TTL_MS = 10 * 60 * 1000;
|
|
9
|
+
const GROUP_REPLY_LATEST_ID = "__latest__";
|
|
10
|
+
function normalizeGroupReplyTarget(rawTarget) {
|
|
11
|
+
if (typeof rawTarget !== "string") {
|
|
12
|
+
return String(rawTarget ?? "").trim();
|
|
13
|
+
}
|
|
14
|
+
return (0, helpers_1.normalizeOutboundTarget)(rawTarget) || rawTarget.trim();
|
|
15
|
+
}
|
|
16
|
+
function buildGroupReplyAddressKey(input) {
|
|
17
|
+
const chatId = normalizeGroupReplyTarget(input.chatId);
|
|
18
|
+
const replyToId = input.replyToId === null || input.replyToId === undefined ? "" : String(input.replyToId).trim();
|
|
19
|
+
if (!chatId || !replyToId) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
return [input.accountId ?? "", chatId, replyToId].join("\n");
|
|
23
|
+
}
|
|
24
|
+
function rememberGroupReplyAddress(input) {
|
|
25
|
+
if (!input.address) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const key = buildGroupReplyAddressKey(input);
|
|
29
|
+
if (!key) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
groupReplyAddresses.set(key, {
|
|
33
|
+
address: input.address,
|
|
34
|
+
expiresAt: Date.now() + GROUP_REPLY_ADDRESS_TTL_MS,
|
|
35
|
+
});
|
|
36
|
+
const latestKey = buildGroupReplyAddressKey({
|
|
37
|
+
...input,
|
|
38
|
+
replyToId: GROUP_REPLY_LATEST_ID,
|
|
39
|
+
});
|
|
40
|
+
if (latestKey) {
|
|
41
|
+
groupReplyAddresses.set(latestKey, {
|
|
42
|
+
address: input.address,
|
|
43
|
+
expiresAt: Date.now() + GROUP_REPLY_ADDRESS_TTL_MS,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function consumeGroupReplyAddress(input) {
|
|
48
|
+
const latestKey = buildGroupReplyAddressKey({
|
|
49
|
+
...input,
|
|
50
|
+
replyToId: GROUP_REPLY_LATEST_ID,
|
|
51
|
+
});
|
|
52
|
+
const key = buildGroupReplyAddressKey(input) ?? latestKey;
|
|
53
|
+
if (!key) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const stored = groupReplyAddresses.get(key);
|
|
57
|
+
if (!stored) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
groupReplyAddresses.delete(key);
|
|
61
|
+
if (latestKey) {
|
|
62
|
+
groupReplyAddresses.delete(latestKey);
|
|
63
|
+
}
|
|
64
|
+
if (stored.expiresAt < Date.now()) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
return stored.address;
|
|
68
|
+
}
|
|
69
|
+
function buildGroupReplyAddress(input) {
|
|
70
|
+
const username = input.senderUsername?.replace(/^@/, "").trim();
|
|
71
|
+
if (username) {
|
|
72
|
+
return `@${username}`;
|
|
73
|
+
}
|
|
74
|
+
const display = input.senderDisplay?.trim();
|
|
75
|
+
if (display && display !== "Telegram") {
|
|
76
|
+
return display;
|
|
77
|
+
}
|
|
78
|
+
return input.senderId;
|
|
79
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hasRecentVisibleGroupReply = hasRecentVisibleGroupReply;
|
|
4
|
+
exports.rememberVisibleGroupReply = rememberVisibleGroupReply;
|
|
5
|
+
const helpers_1 = require("./helpers");
|
|
6
|
+
const recentVisibleGroupReplies = new Map();
|
|
7
|
+
const GROUP_VISIBLE_REPLY_TTL_MS = 10 * 60 * 1000;
|
|
8
|
+
function buildVisibleGroupReplyKey(input) {
|
|
9
|
+
const chatId = (0, helpers_1.normalizeOutboundTarget)(String(input.chatId ?? "").trim());
|
|
10
|
+
const currentMessageId = input.currentMessageId === null || input.currentMessageId === undefined
|
|
11
|
+
? ""
|
|
12
|
+
: String(input.currentMessageId).trim();
|
|
13
|
+
if (!chatId || !currentMessageId) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
return [input.accountId ?? "", chatId, currentMessageId].join("\n");
|
|
17
|
+
}
|
|
18
|
+
function pruneExpiredEntries(now) {
|
|
19
|
+
for (const [key, expiresAt] of recentVisibleGroupReplies.entries()) {
|
|
20
|
+
if (expiresAt <= now) {
|
|
21
|
+
recentVisibleGroupReplies.delete(key);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function hasRecentVisibleGroupReply(input) {
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
pruneExpiredEntries(now);
|
|
28
|
+
const key = buildVisibleGroupReplyKey(input);
|
|
29
|
+
if (!key) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
const expiresAt = recentVisibleGroupReplies.get(key);
|
|
33
|
+
if (!expiresAt) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
if (expiresAt <= now) {
|
|
37
|
+
recentVisibleGroupReplies.delete(key);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
function rememberVisibleGroupReply(input) {
|
|
43
|
+
const key = buildVisibleGroupReplyKey(input);
|
|
44
|
+
if (!key) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
recentVisibleGroupReplies.set(key, Date.now() + GROUP_VISIBLE_REPLY_TTL_MS);
|
|
48
|
+
}
|