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.
@@ -0,0 +1,286 @@
1
+ "use strict";
2
+ /**
3
+ * Reading a window of chat history.
4
+ *
5
+ * The channel could only ever send. An assistant that is expected to summarize
6
+ * what a team wrote during a standup needs to read it, and inbound events do not
7
+ * cover that: under `groupPolicy: "mention"` a message without a mention is
8
+ * dropped at the gate, and nothing is buffered anywhere.
9
+ *
10
+ * Everything here is pure so it can be tested without a Telegram client. The
11
+ * transport call lives in `GramJsClientManager.listMessages`.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.PARTICIPANTS_MAX_LIMIT = exports.PARTICIPANTS_DEFAULT_LIMIT = exports.HISTORY_MAX_LIMIT = exports.HISTORY_DEFAULT_LIMIT = void 0;
15
+ exports.parseTimeBoundary = parseTimeBoundary;
16
+ exports.parseLimit = parseLimit;
17
+ exports.parseMessageId = parseMessageId;
18
+ exports.parseListMessagesParams = parseListMessagesParams;
19
+ exports.parseListParticipantsParams = parseListParticipantsParams;
20
+ exports.buildHistoryQuery = buildHistoryQuery;
21
+ exports.isChatReadable = isChatReadable;
22
+ exports.isWithinWindow = isWithinWindow;
23
+ exports.normalizeHistoryMessage = normalizeHistoryMessage;
24
+ exports.collectHistoryWindow = collectHistoryWindow;
25
+ exports.HISTORY_DEFAULT_LIMIT = 100;
26
+ exports.HISTORY_MAX_LIMIT = 500;
27
+ /**
28
+ * Matches `normalize.ts` and `gramjs-client.ts` deliberately.
29
+ *
30
+ * GramJS carries ids as `big-integer` instances — plain objects whose `typeof`
31
+ * is "object", not native `bigint`. A `typeof value === "bigint"` check misses
32
+ * every one of them, and the failure is silent: `senderId` simply comes back
33
+ * undefined and a standup summary loses the one field that says who wrote.
34
+ *
35
+ * The `[object Object]` guard catches the opposite mistake — passing a whole
36
+ * Peer instead of the id inside it, which would otherwise produce a plausible
37
+ * looking string.
38
+ */
39
+ function toStringId(value) {
40
+ if (value === null || value === undefined)
41
+ return undefined;
42
+ try {
43
+ const text = String(value);
44
+ return text && text !== "[object Object]" ? text : undefined;
45
+ }
46
+ catch {
47
+ return undefined;
48
+ }
49
+ }
50
+ /**
51
+ * Accepts Unix seconds or anything `Date` can parse (ISO 8601 in practice).
52
+ *
53
+ * Milliseconds are converted rather than rejected: a caller reading OpenClaw
54
+ * metadata has millisecond timestamps at hand, and silently treating 1.7e12 as
55
+ * seconds would place the window some fifty thousand years out.
56
+ */
57
+ function parseTimeBoundary(value, field) {
58
+ if (value === undefined || value === null || value === "")
59
+ return undefined;
60
+ if (typeof value === "number" && Number.isFinite(value)) {
61
+ if (value <= 0)
62
+ throw new Error(`clawgram: ${field} must be a positive timestamp`);
63
+ return value >= 10_000_000_000 ? Math.floor(value / 1000) : Math.floor(value);
64
+ }
65
+ if (typeof value === "string") {
66
+ const numeric = Number(value);
67
+ if (Number.isFinite(numeric) && value.trim() !== "") {
68
+ return parseTimeBoundary(numeric, field);
69
+ }
70
+ const parsed = Date.parse(value);
71
+ if (Number.isNaN(parsed)) {
72
+ throw new Error(`clawgram: ${field} is not a valid date: ${value}`);
73
+ }
74
+ return Math.floor(parsed / 1000);
75
+ }
76
+ throw new Error(`clawgram: ${field} must be a timestamp or an ISO 8601 date`);
77
+ }
78
+ /**
79
+ * `limit` is clamped rather than rejected. A model asking for 10000 messages is
80
+ * making a scale mistake, not a security one, and failing the whole call would
81
+ * teach it nothing useful.
82
+ */
83
+ function parseLimit(value) {
84
+ if (value === undefined || value === null || value === "")
85
+ return exports.HISTORY_DEFAULT_LIMIT;
86
+ const numeric = typeof value === "number" ? value : Number(value);
87
+ if (!Number.isFinite(numeric)) {
88
+ throw new Error("clawgram: limit must be a number");
89
+ }
90
+ if (numeric < 1) {
91
+ throw new Error("clawgram: limit must be at least 1");
92
+ }
93
+ return Math.min(Math.floor(numeric), exports.HISTORY_MAX_LIMIT);
94
+ }
95
+ /** Message ids are integers, and confusing one with a date is the bug this exists to prevent. */
96
+ function parseMessageId(value, field) {
97
+ if (value === undefined || value === null || value === "")
98
+ return undefined;
99
+ const numeric = typeof value === "number" ? value : Number(value);
100
+ if (!Number.isFinite(numeric) || !Number.isInteger(numeric) || numeric < 1) {
101
+ throw new Error(`clawgram: ${field} must be a positive message id`);
102
+ }
103
+ return numeric;
104
+ }
105
+ function parseListMessagesParams(params) {
106
+ const rawTarget = params.chatId ?? params.target ?? params.to ?? params.chat;
107
+ const target = typeof rawTarget === "string" ? rawTarget.trim() : "";
108
+ if (!target) {
109
+ throw new Error("clawgram: list requires a chatId");
110
+ }
111
+ // Only `since`/`until` carry dates. `before`/`after` are NOT accepted here:
112
+ // in OpenClaw's vocabulary they are message ids, and `openclaw message read
113
+ // --before 12345` would otherwise parse an id as a Unix timestamp and quietly
114
+ // place the window in 1970.
115
+ const since = parseTimeBoundary(params.since, "since");
116
+ const until = parseTimeBoundary(params.until, "until");
117
+ if (since !== undefined && until !== undefined && since > until) {
118
+ throw new Error("clawgram: since must not be later than until");
119
+ }
120
+ return {
121
+ target,
122
+ limit: parseLimit(params.limit),
123
+ since,
124
+ until,
125
+ minId: parseMessageId(params.after, "after"),
126
+ maxId: parseMessageId(params.before, "before"),
127
+ };
128
+ }
129
+ exports.PARTICIPANTS_DEFAULT_LIMIT = 200;
130
+ exports.PARTICIPANTS_MAX_LIMIT = 1000;
131
+ /**
132
+ * Membership is asked for by chat, so a target is required. `limit` is clamped
133
+ * for the same reason it is clamped when reading history: a large group must
134
+ * not silently turn into an unbounded response.
135
+ */
136
+ function parseListParticipantsParams(params) {
137
+ const rawTarget = params.chatId ?? params.target ?? params.to ?? params.chat;
138
+ const target = typeof rawTarget === "string" ? rawTarget.trim() : "";
139
+ if (!target) {
140
+ throw new Error("clawgram: participants requires a chatId");
141
+ }
142
+ const includeNames = params.includeNames === true || params.includeNames === "true";
143
+ const rawLimit = params.limit;
144
+ if (rawLimit === undefined || rawLimit === null || rawLimit === "") {
145
+ return { target, limit: exports.PARTICIPANTS_DEFAULT_LIMIT, includeNames };
146
+ }
147
+ const parsed = Number(rawLimit);
148
+ if (!Number.isFinite(parsed) || parsed <= 0) {
149
+ throw new Error("clawgram: participants limit must be a positive number");
150
+ }
151
+ return { target, limit: Math.min(Math.floor(parsed), exports.PARTICIPANTS_MAX_LIMIT), includeNames };
152
+ }
153
+ /**
154
+ * Builds the GramJS query for a window.
155
+ *
156
+ * `offsetDate` is Unix seconds (`DateLike = number`) and Telegram documents it
157
+ * exclusive — "messages previous to this date". `until` here is inclusive, so
158
+ * the bound is shifted by a second and the exact bound is re-applied by
159
+ * `isWithinWindow` afterwards. There is no server-side lower bound in this
160
+ * call, which is why `limit` is the real guard: `since` can only be enforced
161
+ * after the fact.
162
+ */
163
+ function buildHistoryQuery(args) {
164
+ const query = { limit: args.limit };
165
+ if (args.until !== undefined) {
166
+ query.offsetDate = args.until + 1;
167
+ }
168
+ if (args.minId !== undefined) {
169
+ query.minId = args.minId;
170
+ }
171
+ if (args.maxId !== undefined) {
172
+ query.maxId = args.maxId;
173
+ }
174
+ return query;
175
+ }
176
+ function normalizeChatKey(value) {
177
+ return String(value ?? "").trim().replace(/^@/, "").toLowerCase();
178
+ }
179
+ /**
180
+ * Read scope for the account, checked before any history call.
181
+ *
182
+ * Sending is gated by whoever asks; reading is not, so the scope has to be
183
+ * declared. This account belongs to a person, not a bot: it sits in family
184
+ * chats and private conversations alongside the work ones, and a model that
185
+ * has been talked into reading the wrong chat pulls that correspondence into
186
+ * a prompt — and from there into whatever it publishes next.
187
+ *
188
+ * An absent list means "no restriction", which keeps the plugin generally
189
+ * usable; a deployment that cares sets `readChats` and gets a hard boundary
190
+ * rather than a sentence in a prompt that a model may be argued out of.
191
+ */
192
+ function isChatReadable(target, readChats) {
193
+ if (readChats === undefined || readChats === null)
194
+ return true;
195
+ const entries = (Array.isArray(readChats) ? readChats : [readChats])
196
+ .map(normalizeChatKey)
197
+ .filter(Boolean);
198
+ // An empty list is a configured empty list — deny, rather than silently
199
+ // reading everything because someone left brackets behind.
200
+ if (entries.length === 0)
201
+ return false;
202
+ if (entries.includes("*"))
203
+ return true;
204
+ return entries.includes(normalizeChatKey(target));
205
+ }
206
+ function isWithinWindow(timestamp, since, until) {
207
+ // A message without a date cannot be placed in the window. Keeping it only
208
+ // when the window is open avoids silently widening a bounded request.
209
+ if (timestamp === undefined)
210
+ return since === undefined && until === undefined;
211
+ if (since !== undefined && timestamp < since)
212
+ return false;
213
+ if (until !== undefined && timestamp > until)
214
+ return false;
215
+ return true;
216
+ }
217
+ function readSender(msg, key) {
218
+ const value = msg?.sender?.[key] ?? msg?._sender?.[key];
219
+ return typeof value === "string" && value.length > 0 ? value : undefined;
220
+ }
221
+ function resolveSenderDisplay(msg) {
222
+ const first = readSender(msg, "firstName");
223
+ const last = readSender(msg, "lastName");
224
+ const joined = [first, last].filter(Boolean).join(" ").trim();
225
+ if (joined)
226
+ return joined;
227
+ return readSender(msg, "title") ?? readSender(msg, "username");
228
+ }
229
+ /**
230
+ * Deliberately drops `raw`. Inbound events carry it because the runtime may need
231
+ * the original object; history is read straight into a model's context, where an
232
+ * unbounded GramJS structure is both expensive and a way for internals to leak
233
+ * into a prompt.
234
+ */
235
+ function normalizeHistoryMessage(msg, fallbackChatId) {
236
+ const messageId = toStringId(msg?.id);
237
+ if (!messageId)
238
+ return null;
239
+ const text = typeof msg?.message === "string" ? msg.message :
240
+ typeof msg?.text === "string" ? msg.text :
241
+ undefined;
242
+ const rawDate = msg?.date;
243
+ let timestamp;
244
+ if (rawDate instanceof Date) {
245
+ timestamp = Math.floor(rawDate.getTime() / 1000);
246
+ }
247
+ else if (typeof rawDate === "number" && Number.isFinite(rawDate)) {
248
+ timestamp = rawDate >= 10_000_000_000 ? Math.floor(rawDate / 1000) : Math.floor(rawDate);
249
+ }
250
+ return {
251
+ messageId,
252
+ chatId: toStringId(msg?.chatId) ?? fallbackChatId,
253
+ senderId: toStringId(msg?.senderId) ?? toStringId(msg?.fromId?.userId) ?? toStringId(msg?.fromId?.channelId),
254
+ senderUsername: readSender(msg, "username"),
255
+ senderDisplay: resolveSenderDisplay(msg),
256
+ text,
257
+ timestamp,
258
+ sentAt: timestamp === undefined ? undefined : new Date(timestamp * 1000).toISOString(),
259
+ replyToMessageId: toStringId(msg?.replyTo?.replyToMsgId) ?? toStringId(msg?.replyToMsgId),
260
+ messageThreadId: toStringId(msg?.replyTo?.replyToTopId) ?? toStringId(msg?.replyToTopId),
261
+ isOutgoing: msg?.out === true,
262
+ };
263
+ }
264
+ /**
265
+ * Normalizes, drops what falls outside the window, and returns oldest first —
266
+ * reading order, which is what a summary needs. Telegram hands back newest
267
+ * first, so an unsorted result would have the model reconstruct a conversation
268
+ * backwards.
269
+ */
270
+ function collectHistoryWindow(messages, options = {}) {
271
+ const collected = [];
272
+ for (const raw of messages ?? []) {
273
+ const normalized = normalizeHistoryMessage(raw, options.fallbackChatId);
274
+ if (!normalized)
275
+ continue;
276
+ if (!isWithinWindow(normalized.timestamp, options.since, options.until))
277
+ continue;
278
+ collected.push(normalized);
279
+ }
280
+ return collected.sort((a, b) => {
281
+ const byTime = (a.timestamp ?? 0) - (b.timestamp ?? 0);
282
+ if (byTime !== 0)
283
+ return byTime;
284
+ return Number(a.messageId) - Number(b.messageId);
285
+ });
286
+ }
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const channel_1 = require("./channel");
4
+ const cli_1 = require("./cli");
5
+ const plugin = {
6
+ id: 'clawgram',
7
+ name: 'Clawgram',
8
+ description: "Connect your personal Telegram account to OpenClaw via MTProto. Your AI assistant responds as you.",
9
+ register(api) {
10
+ const runtimes = new Map();
11
+ api.registerCli(({ program, config }) => {
12
+ (0, cli_1.registerTelegramUserbotCli)(program, config);
13
+ }, {
14
+ commands: (0, cli_1.getTelegramUserbotCliDescriptors)().map((entry) => entry.name),
15
+ descriptors: (0, cli_1.getTelegramUserbotCliDescriptors)()
16
+ });
17
+ api.registerChannel({ plugin: (0, channel_1.createChannelPlugin)(runtimes) });
18
+ }
19
+ };
20
+ exports.default = plugin;
package/dist/joins.js ADDED
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.JOINS_JOURNAL_MAX_RECORDS = exports.JOINS_MAX_LIMIT = exports.JOINS_DEFAULT_LIMIT = void 0;
4
+ exports.parseJoinEvent = parseJoinEvent;
5
+ exports.resolveJoinsJournalPath = resolveJoinsJournalPath;
6
+ exports.readJoinRecords = readJoinRecords;
7
+ exports.appendJoinRecord = appendJoinRecord;
8
+ exports.selectJoinRecords = selectJoinRecords;
9
+ exports.parseJoinsParams = parseJoinsParams;
10
+ /**
11
+ * "Someone added this account to a chat" — the one event an assistant needs to
12
+ * learn where it is expected to work, and who put it there.
13
+ *
14
+ * Telegram delivers it as a service message, which the `NewMessage` subscription
15
+ * deliberately drops, so it is recognised here from raw updates instead. Only
16
+ * additions of THIS account are recorded: who else joins a chat is none of our
17
+ * business and would turn the journal into surveillance.
18
+ */
19
+ const node_fs_1 = require("node:fs");
20
+ const node_os_1 = require("node:os");
21
+ const node_path_1 = require("node:path");
22
+ const normalize_js_1 = require("./normalize.js");
23
+ exports.JOINS_DEFAULT_LIMIT = 50;
24
+ exports.JOINS_MAX_LIMIT = 500;
25
+ /** The journal answers "recently", not "since the beginning of time". */
26
+ exports.JOINS_JOURNAL_MAX_RECORDS = 2000;
27
+ function chatIdOf(rawMessage) {
28
+ return (0, normalize_js_1.toStringId)(rawMessage?.chatId) ??
29
+ (0, normalize_js_1.toPeerChannelId)(rawMessage?.peerId?.channelId) ??
30
+ (0, normalize_js_1.toPeerChatId)(rawMessage?.peerId?.chatId);
31
+ }
32
+ function atOf(rawMessage) {
33
+ const seconds = Number(rawMessage?.date);
34
+ if (Number.isFinite(seconds) && seconds > 0) {
35
+ return new Date(seconds * 1000).toISOString();
36
+ }
37
+ // A service message without a usable date is still a real event; it is
38
+ // stamped on arrival rather than dropped.
39
+ return new Date().toISOString();
40
+ }
41
+ function containsId(users, selfId) {
42
+ if (!Array.isArray(users))
43
+ return false;
44
+ return users.some((entry) => (0, normalize_js_1.toStringId)(entry) === selfId);
45
+ }
46
+ /**
47
+ * Returns the join event when THIS account was added, otherwise undefined.
48
+ * Everything is read defensively: raw updates arrive untyped and malformed
49
+ * input must not take the channel down.
50
+ */
51
+ function parseJoinEvent(rawMessage, selfId) {
52
+ if (!selfId)
53
+ return undefined;
54
+ const action = rawMessage?.action;
55
+ const className = typeof action?.className === "string" ? action.className : undefined;
56
+ if (!className)
57
+ return undefined;
58
+ const chatId = chatIdOf(rawMessage);
59
+ if (!chatId)
60
+ return undefined;
61
+ const base = {
62
+ chatId,
63
+ at: atOf(rawMessage),
64
+ messageId: (0, normalize_js_1.toStringId)(rawMessage?.id),
65
+ };
66
+ const fromId = (0, normalize_js_1.toStringId)(rawMessage?.fromId?.userId) ?? (0, normalize_js_1.toStringId)(rawMessage?.senderId);
67
+ if (className === "MessageActionChatAddUser") {
68
+ if (!containsId(action?.users, selfId))
69
+ return undefined;
70
+ return { ...base, via: "added", inviterId: fromId };
71
+ }
72
+ if (className === "MessageActionChatJoinedByLink") {
73
+ // The same message is emitted when other people join by link, so it only
74
+ // concerns us when we are the one who joined.
75
+ if (fromId !== undefined && fromId !== selfId)
76
+ return undefined;
77
+ return { ...base, via: "link", inviterId: (0, normalize_js_1.toStringId)(action?.inviterId) };
78
+ }
79
+ if (className === "MessageActionChatCreate") {
80
+ if (!containsId(action?.users, selfId))
81
+ return undefined;
82
+ return { ...base, via: "created", inviterId: fromId };
83
+ }
84
+ return undefined;
85
+ }
86
+ function resolveJoinsJournalPath(accountCfg, accountId) {
87
+ const configured = accountCfg?.joinsJournalPath;
88
+ if (typeof configured === "string" && configured.trim().length > 0) {
89
+ return configured.trim();
90
+ }
91
+ const safeAccount = accountId.replace(/[^A-Za-z0-9._-]/g, "_");
92
+ return (0, node_path_1.join)((0, node_os_1.homedir)(), ".openclaw", "state", "clawgram", `joins-${safeAccount}.jsonl`);
93
+ }
94
+ function readJoinRecords(path) {
95
+ if (!(0, node_fs_1.existsSync)(path))
96
+ return [];
97
+ return (0, node_fs_1.readFileSync)(path, "utf8")
98
+ .split("\n")
99
+ .filter((line) => line.trim().length > 0)
100
+ .map((line) => {
101
+ try {
102
+ return JSON.parse(line);
103
+ }
104
+ catch {
105
+ return undefined;
106
+ }
107
+ })
108
+ .filter((entry) => entry !== undefined);
109
+ }
110
+ /**
111
+ * Appends one event, keeping the journal bounded. A repeated add to the same
112
+ * chat is kept: being re-added after a removal is itself information.
113
+ */
114
+ function appendJoinRecord(path, event) {
115
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
116
+ (0, node_fs_1.appendFileSync)(path, JSON.stringify(event) + "\n");
117
+ const records = readJoinRecords(path);
118
+ if (records.length > exports.JOINS_JOURNAL_MAX_RECORDS) {
119
+ const kept = records.slice(records.length - exports.JOINS_JOURNAL_MAX_RECORDS);
120
+ (0, node_fs_1.writeFileSync)(path, kept.map((entry) => JSON.stringify(entry)).join("\n") + "\n");
121
+ }
122
+ }
123
+ function selectJoinRecords(records, args) {
124
+ const filtered = args.since
125
+ ? records.filter((entry) => typeof entry.at === "string" && entry.at >= args.since)
126
+ : records;
127
+ return filtered.slice(Math.max(0, filtered.length - args.limit));
128
+ }
129
+ function parseJoinsParams(params) {
130
+ const rawSince = params.since;
131
+ let since;
132
+ if (rawSince !== undefined && rawSince !== null && rawSince !== "") {
133
+ if (typeof rawSince !== "string" || Number.isNaN(Date.parse(rawSince))) {
134
+ throw new Error("clawgram: joins since must be an ISO-8601 timestamp");
135
+ }
136
+ since = new Date(rawSince).toISOString();
137
+ }
138
+ const rawLimit = params.limit;
139
+ if (rawLimit === undefined || rawLimit === null || rawLimit === "") {
140
+ return { since, limit: exports.JOINS_DEFAULT_LIMIT };
141
+ }
142
+ const parsed = Number(rawLimit);
143
+ if (!Number.isFinite(parsed) || parsed <= 0) {
144
+ throw new Error("clawgram: joins limit must be a positive number");
145
+ }
146
+ return { since, limit: Math.min(Math.floor(parsed), exports.JOINS_MAX_LIMIT) };
147
+ }
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toStringId = toStringId;
4
+ exports.toPeerChatId = toPeerChatId;
5
+ exports.toPeerChannelId = toPeerChannelId;
6
+ exports.normalizeTelegramEvent = normalizeTelegramEvent;
7
+ function toStringId(value) {
8
+ if (value === null || value === undefined)
9
+ return undefined;
10
+ try {
11
+ return String(value);
12
+ }
13
+ catch {
14
+ return undefined;
15
+ }
16
+ }
17
+ function inferChatType(chatId) {
18
+ if (chatId.startsWith("-100"))
19
+ return "channel";
20
+ if (chatId.startsWith("-"))
21
+ return "group";
22
+ return "direct";
23
+ }
24
+ function inferTelegramChatType(msg, chatId) {
25
+ if (msg?.isGroup === true)
26
+ return "group";
27
+ if (msg?.isChannel === true)
28
+ return "channel";
29
+ if (chatId.startsWith("-100") && msg?.post !== true)
30
+ return "group";
31
+ return inferChatType(chatId);
32
+ }
33
+ function toPeerChatId(value) {
34
+ const id = toStringId(value);
35
+ if (!id)
36
+ return undefined;
37
+ return `-${id.replace(/^-/, "")}`;
38
+ }
39
+ function toPeerChannelId(value) {
40
+ const id = toStringId(value);
41
+ if (!id)
42
+ return undefined;
43
+ return `-100${id.replace(/^-100|-/, "")}`;
44
+ }
45
+ function toTimestamp(value) {
46
+ if (value instanceof Date) {
47
+ return value.getTime();
48
+ }
49
+ if (typeof value === "number" && Number.isFinite(value)) {
50
+ // GramJS message.date may arrive as a Unix timestamp in seconds.
51
+ // OpenClaw expects millisecond timestamps for prompt/runtime metadata.
52
+ return value < 10_000_000_000 ? value * 1000 : value;
53
+ }
54
+ return undefined;
55
+ }
56
+ function resolveMessageThreadId(msg) {
57
+ const isForumTopic = msg?.replyTo?.forumTopic === true || msg?.forumTopic === true;
58
+ if (!isForumTopic) {
59
+ return undefined;
60
+ }
61
+ const topId = toStringId(msg?.replyTo?.replyToTopId) ??
62
+ toStringId(msg?.replyToTopId);
63
+ if (topId) {
64
+ return topId;
65
+ }
66
+ return (toStringId(msg?.replyTo?.replyToMsgId) ??
67
+ toStringId(msg?.replyToMsgId));
68
+ }
69
+ function normalizeTelegramEvent(event, accountId) {
70
+ const msg = event?.message;
71
+ if (!msg)
72
+ return null;
73
+ const chatId = toStringId(msg.chatId) ??
74
+ toStringId(event?.chatId) ??
75
+ toPeerChannelId(msg.peerId?.channelId) ??
76
+ toPeerChatId(msg.peerId?.chatId) ??
77
+ toStringId(msg.peerId?.userId);
78
+ const messageId = toStringId(msg.id);
79
+ if (!chatId || !messageId)
80
+ return null;
81
+ const senderId = toStringId(msg.senderId) ??
82
+ toStringId(msg.fromId?.userId) ??
83
+ toStringId(msg.fromId?.channelId);
84
+ const replyToMessageId = toStringId(msg.replyTo?.replyToMsgId) ??
85
+ toStringId(msg.replyToMsgId);
86
+ const messageThreadId = resolveMessageThreadId(msg);
87
+ const text = typeof msg.message === "string"
88
+ ? msg.message
89
+ : typeof msg.text === "string"
90
+ ? msg.text
91
+ : undefined;
92
+ const chatType = inferTelegramChatType(msg, chatId);
93
+ const senderUsername = typeof msg.sender?.username === "string"
94
+ ? msg.sender.username
95
+ : typeof msg._sender?.username === "string"
96
+ ? msg._sender.username
97
+ : undefined;
98
+ const senderDisplay = typeof msg.sender?.firstName === "string"
99
+ ? [msg.sender.firstName, msg.sender.lastName].filter(Boolean).join(" ").trim()
100
+ : typeof msg._sender?.firstName === "string"
101
+ ? [msg._sender.firstName, msg._sender.lastName].filter(Boolean).join(" ").trim()
102
+ : undefined;
103
+ const replyTarget = msg.inputChat ??
104
+ msg._inputChat ??
105
+ msg.inputSender ??
106
+ msg._inputSender ??
107
+ msg.peerId;
108
+ return {
109
+ channel: "clawgram",
110
+ accountId,
111
+ chatId,
112
+ messageThreadId,
113
+ senderId,
114
+ senderUsername,
115
+ senderDisplay: senderDisplay || undefined,
116
+ messageId,
117
+ text,
118
+ replyToMessageId,
119
+ chatType,
120
+ timestamp: toTimestamp(msg.date),
121
+ isOutgoing: Boolean(msg.out),
122
+ replyTarget,
123
+ raw: event
124
+ };
125
+ }
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveProxyConfig = resolveProxyConfig;
4
+ exports.buildTelegramClientOptions = buildTelegramClientOptions;
5
+ exports.describeProxy = describeProxy;
6
+ const CONNECTION_RETRIES = 5;
7
+ function isPlainObject(value) {
8
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9
+ }
10
+ function toFiniteNumber(value) {
11
+ if (typeof value === "number") {
12
+ return value;
13
+ }
14
+ if (typeof value === "string" && value.trim()) {
15
+ return Number(value.trim());
16
+ }
17
+ return Number.NaN;
18
+ }
19
+ function resolveProxyHost(value) {
20
+ const host = typeof value === "string" ? value.trim() : "";
21
+ if (!host) {
22
+ throw new Error("clawgram: proxy.ip must be a non-empty hostname or IP address.");
23
+ }
24
+ return host;
25
+ }
26
+ function resolveProxyPort(value) {
27
+ const port = toFiniteNumber(value);
28
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
29
+ throw new Error("clawgram: proxy.port must be an integer between 1 and 65535.");
30
+ }
31
+ return port;
32
+ }
33
+ function resolveSocksType(value) {
34
+ const socksType = toFiniteNumber(value);
35
+ if (socksType !== 4 && socksType !== 5) {
36
+ throw new Error("clawgram: proxy.socksType must be 4 (SOCKS4) or 5 (SOCKS5).");
37
+ }
38
+ return socksType;
39
+ }
40
+ function resolveProxyCredential(value, field) {
41
+ if (value === undefined || value === null) {
42
+ return undefined;
43
+ }
44
+ if (typeof value !== "string") {
45
+ throw new Error(`clawgram: proxy.${field} must be a string.`);
46
+ }
47
+ return value.trim() ? value : undefined;
48
+ }
49
+ function resolveProxyTimeout(value) {
50
+ if (value === undefined || value === null) {
51
+ return undefined;
52
+ }
53
+ const timeout = toFiniteNumber(value);
54
+ if (!Number.isFinite(timeout) || timeout <= 0) {
55
+ throw new Error("clawgram: proxy.timeout must be a positive number of seconds.");
56
+ }
57
+ return timeout;
58
+ }
59
+ /**
60
+ * Normalizes and validates the optional per-account `proxy` config.
61
+ * Returns `undefined` when no proxy is configured; throws when one is
62
+ * configured but unusable, so a broken proxy never silently falls back to a
63
+ * direct connection.
64
+ */
65
+ function resolveProxyConfig(value) {
66
+ if (value === undefined || value === null) {
67
+ return undefined;
68
+ }
69
+ if (!isPlainObject(value)) {
70
+ throw new Error("clawgram: proxy must be an object.");
71
+ }
72
+ const ip = resolveProxyHost(value.ip);
73
+ const port = resolveProxyPort(value.port);
74
+ const socksType = resolveSocksType(value.socksType);
75
+ const username = resolveProxyCredential(value.username, "username");
76
+ const password = resolveProxyCredential(value.password, "password");
77
+ const timeout = resolveProxyTimeout(value.timeout);
78
+ return {
79
+ ip,
80
+ port,
81
+ socksType,
82
+ ...(username ? { username } : {}),
83
+ ...(password ? { password } : {}),
84
+ ...(timeout !== undefined ? { timeout } : {}),
85
+ };
86
+ }
87
+ /**
88
+ * Builds the TelegramClient options. Without a proxy the options stay exactly as
89
+ * before this feature existed.
90
+ */
91
+ function buildTelegramClientOptions(proxy) {
92
+ const resolved = resolveProxyConfig(proxy);
93
+ if (!resolved) {
94
+ return {
95
+ connectionRetries: CONNECTION_RETRIES,
96
+ };
97
+ }
98
+ return {
99
+ connectionRetries: CONNECTION_RETRIES,
100
+ proxy: resolved,
101
+ };
102
+ }
103
+ /** Credential-free proxy summary safe to log. */
104
+ function describeProxy(proxy) {
105
+ return proxy ? `socks${proxy.socksType}` : undefined;
106
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });