lazy-gravity 0.2.0 → 0.3.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.
Files changed (56) hide show
  1. package/README.md +76 -15
  2. package/dist/bin/commands/doctor.js +19 -2
  3. package/dist/bin/commands/setup.js +286 -70
  4. package/dist/bot/eventRouter.js +70 -0
  5. package/dist/bot/index.js +353 -147
  6. package/dist/bot/telegramCommands.js +428 -0
  7. package/dist/bot/telegramMessageHandler.js +304 -0
  8. package/dist/bot/telegramProjectCommand.js +137 -0
  9. package/dist/bot/workspaceQueue.js +61 -0
  10. package/dist/commands/joinCommandHandler.js +4 -1
  11. package/dist/database/telegramBindingRepository.js +97 -0
  12. package/dist/database/userPreferenceRepository.js +46 -1
  13. package/dist/events/interactionCreateHandler.js +36 -0
  14. package/dist/events/messageCreateHandler.js +11 -7
  15. package/dist/handlers/approvalButtonAction.js +99 -0
  16. package/dist/handlers/autoAcceptButtonAction.js +43 -0
  17. package/dist/handlers/buttonHandler.js +55 -0
  18. package/dist/handlers/commandHandler.js +44 -0
  19. package/dist/handlers/errorPopupButtonAction.js +137 -0
  20. package/dist/handlers/messageHandler.js +70 -0
  21. package/dist/handlers/modeSelectAction.js +63 -0
  22. package/dist/handlers/modelButtonAction.js +102 -0
  23. package/dist/handlers/planningButtonAction.js +118 -0
  24. package/dist/handlers/selectHandler.js +41 -0
  25. package/dist/handlers/templateButtonAction.js +54 -0
  26. package/dist/platform/adapter.js +8 -0
  27. package/dist/platform/discord/discordAdapter.js +99 -0
  28. package/dist/platform/discord/index.js +15 -0
  29. package/dist/platform/discord/wrappers.js +331 -0
  30. package/dist/platform/index.js +18 -0
  31. package/dist/platform/richContentBuilder.js +76 -0
  32. package/dist/platform/telegram/index.js +16 -0
  33. package/dist/platform/telegram/telegramAdapter.js +195 -0
  34. package/dist/platform/telegram/telegramFormatter.js +134 -0
  35. package/dist/platform/telegram/wrappers.js +329 -0
  36. package/dist/platform/types.js +28 -0
  37. package/dist/services/approvalDetector.js +15 -2
  38. package/dist/services/cdpBridgeManager.js +91 -146
  39. package/dist/services/defaultModelApplicator.js +54 -0
  40. package/dist/services/modeService.js +16 -1
  41. package/dist/services/modelService.js +57 -16
  42. package/dist/services/notificationSender.js +149 -0
  43. package/dist/services/responseMonitor.js +1 -2
  44. package/dist/ui/autoAcceptUi.js +37 -0
  45. package/dist/ui/modeUi.js +38 -1
  46. package/dist/ui/modelsUi.js +96 -0
  47. package/dist/ui/outputUi.js +32 -0
  48. package/dist/ui/projectListUi.js +55 -0
  49. package/dist/ui/screenshotUi.js +26 -0
  50. package/dist/ui/sessionPickerUi.js +35 -1
  51. package/dist/ui/templateUi.js +41 -0
  52. package/dist/utils/configLoader.js +63 -12
  53. package/dist/utils/lockfile.js +5 -5
  54. package/dist/utils/logger.js +7 -0
  55. package/dist/utils/telegramImageHandler.js +127 -0
  56. package/package.json +4 -2
@@ -0,0 +1,331 @@
1
+ "use strict";
2
+ /**
3
+ * Wrapper functions to convert discord.js types to/from platform types.
4
+ *
5
+ * These functions form the boundary between the discord.js library and
6
+ * the platform-agnostic core. All conversions are pure (no mutation).
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.toDiscordButtonStyle = toDiscordButtonStyle;
10
+ exports.toDiscordPayload = toDiscordPayload;
11
+ exports.wrapDiscordUser = wrapDiscordUser;
12
+ exports.wrapDiscordSentMessage = wrapDiscordSentMessage;
13
+ exports.wrapDiscordChannel = wrapDiscordChannel;
14
+ exports.wrapDiscordMessage = wrapDiscordMessage;
15
+ exports.wrapDiscordButton = wrapDiscordButton;
16
+ exports.wrapDiscordSelect = wrapDiscordSelect;
17
+ exports.wrapDiscordCommand = wrapDiscordCommand;
18
+ const discord_js_1 = require("discord.js");
19
+ // ---------------------------------------------------------------------------
20
+ // Style mapping
21
+ // ---------------------------------------------------------------------------
22
+ const BUTTON_STYLE_MAP = {
23
+ primary: discord_js_1.ButtonStyle.Primary,
24
+ secondary: discord_js_1.ButtonStyle.Secondary,
25
+ success: discord_js_1.ButtonStyle.Success,
26
+ danger: discord_js_1.ButtonStyle.Danger,
27
+ };
28
+ /** Map a platform ButtonStyle to its discord.js equivalent. */
29
+ function toDiscordButtonStyle(style) {
30
+ return BUTTON_STYLE_MAP[style];
31
+ }
32
+ // ---------------------------------------------------------------------------
33
+ // Payload conversion
34
+ // ---------------------------------------------------------------------------
35
+ /**
36
+ * Convert a platform RichContent to a discord.js EmbedBuilder.
37
+ * Returns a new EmbedBuilder instance.
38
+ */
39
+ function toDiscordEmbed(rc) {
40
+ const embed = new discord_js_1.EmbedBuilder();
41
+ if (rc.title !== undefined) {
42
+ embed.setTitle(rc.title);
43
+ }
44
+ if (rc.description !== undefined) {
45
+ embed.setDescription(rc.description);
46
+ }
47
+ if (rc.color !== undefined) {
48
+ embed.setColor(rc.color);
49
+ }
50
+ if (rc.fields !== undefined) {
51
+ for (const field of rc.fields) {
52
+ embed.addFields({ name: field.name, value: field.value, inline: field.inline });
53
+ }
54
+ }
55
+ if (rc.footer !== undefined) {
56
+ embed.setFooter({ text: rc.footer });
57
+ }
58
+ if (rc.timestamp !== undefined) {
59
+ embed.setTimestamp(rc.timestamp);
60
+ }
61
+ if (rc.thumbnailUrl !== undefined) {
62
+ embed.setThumbnail(rc.thumbnailUrl);
63
+ }
64
+ if (rc.imageUrl !== undefined) {
65
+ embed.setImage(rc.imageUrl);
66
+ }
67
+ return embed;
68
+ }
69
+ /**
70
+ * Convert platform ComponentRow[] to discord.js ActionRowBuilder[].
71
+ * Each row produces one ActionRowBuilder containing buttons or a select menu.
72
+ */
73
+ function toDiscordComponents(rows) {
74
+ return rows.map((row) => {
75
+ const hasButton = row.components.some((c) => c.type === 'button');
76
+ const hasSelect = row.components.some((c) => c.type === 'selectMenu');
77
+ if (hasButton && hasSelect) {
78
+ throw new Error('Discord does not allow mixing buttons and select menus in the same ActionRow.');
79
+ }
80
+ const actionRow = new discord_js_1.ActionRowBuilder();
81
+ for (const comp of row.components) {
82
+ if (comp.type === 'button') {
83
+ const button = new discord_js_1.ButtonBuilder()
84
+ .setCustomId(comp.customId)
85
+ .setLabel(comp.label)
86
+ .setStyle(toDiscordButtonStyle(comp.style));
87
+ if (comp.disabled === true) {
88
+ button.setDisabled(true);
89
+ }
90
+ actionRow.addComponents(button);
91
+ }
92
+ else if (comp.type === 'selectMenu') {
93
+ const select = new discord_js_1.StringSelectMenuBuilder()
94
+ .setCustomId(comp.customId)
95
+ .addOptions(comp.options.map((opt) => ({
96
+ label: opt.label,
97
+ value: opt.value,
98
+ description: opt.description,
99
+ default: opt.isDefault,
100
+ })));
101
+ if (comp.placeholder !== undefined) {
102
+ select.setPlaceholder(comp.placeholder);
103
+ }
104
+ actionRow.addComponents(select);
105
+ }
106
+ }
107
+ return actionRow;
108
+ });
109
+ }
110
+ /**
111
+ * Convert a platform MessagePayload to discord.js message send/reply options.
112
+ *
113
+ * This is the central conversion point used by wrapDiscordChannel.send(),
114
+ * wrapDiscordMessage.reply(), and interaction reply/update methods.
115
+ *
116
+ * @param payload The platform-agnostic message payload.
117
+ * @param opts Conversion options. Only interaction callers should set
118
+ * `allowEphemeral: true`.
119
+ */
120
+ function toDiscordPayload(payload, opts = {}) {
121
+ const result = {};
122
+ if (payload.text !== undefined) {
123
+ result.content = payload.text;
124
+ }
125
+ if (payload.richContent !== undefined) {
126
+ result.embeds = [toDiscordEmbed(payload.richContent)];
127
+ }
128
+ if (payload.components !== undefined && payload.components.length > 0) {
129
+ result.components = toDiscordComponents(payload.components);
130
+ }
131
+ if (payload.files !== undefined && payload.files.length > 0) {
132
+ result.files = payload.files.map((f) => new discord_js_1.AttachmentBuilder(f.data, { name: f.name }));
133
+ }
134
+ if (opts.allowEphemeral === true && payload.ephemeral === true) {
135
+ result.flags = discord_js_1.MessageFlags.Ephemeral;
136
+ }
137
+ return result;
138
+ }
139
+ /** Reusable opts constant for interaction callers that allow ephemeral. */
140
+ const EPHEMERAL_ALLOWED = Object.freeze({ allowEphemeral: true });
141
+ /**
142
+ * Build a minimal PlatformChannel fallback when `interaction.channel` is null
143
+ * (e.g. DMs or uncached channels). Uses `interaction.channelId` which is
144
+ * always available as a string.
145
+ */
146
+ function buildFallbackChannel(channelId) {
147
+ return {
148
+ id: channelId,
149
+ platform: 'discord',
150
+ name: undefined,
151
+ async send() {
152
+ throw new Error(`Cannot send to channel ${channelId}: channel object is not available (DM or uncached)`);
153
+ },
154
+ };
155
+ }
156
+ // ---------------------------------------------------------------------------
157
+ // Entity wrappers
158
+ // ---------------------------------------------------------------------------
159
+ /** Wrap a discord.js User as a PlatformUser. */
160
+ function wrapDiscordUser(user) {
161
+ return {
162
+ id: user.id,
163
+ platform: 'discord',
164
+ username: user.username,
165
+ displayName: user.displayName ?? undefined,
166
+ isBot: user.bot,
167
+ };
168
+ }
169
+ /** Wrap a discord.js Message as a PlatformSentMessage (for edit/delete). */
170
+ function wrapDiscordSentMessage(msg) {
171
+ return {
172
+ id: msg.id,
173
+ platform: 'discord',
174
+ channelId: msg.channelId,
175
+ async edit(payload) {
176
+ const edited = await msg.edit(toDiscordPayload(payload));
177
+ return wrapDiscordSentMessage(edited);
178
+ },
179
+ async delete() {
180
+ await msg.delete();
181
+ },
182
+ };
183
+ }
184
+ /** Wrap a discord.js TextChannel (or any channel with send()) as a PlatformChannel. */
185
+ function wrapDiscordChannel(channel) {
186
+ return {
187
+ id: channel.id,
188
+ platform: 'discord',
189
+ name: 'name' in channel ? channel.name : undefined,
190
+ async send(payload) {
191
+ const sent = await channel.send(toDiscordPayload(payload));
192
+ return wrapDiscordSentMessage(sent);
193
+ },
194
+ };
195
+ }
196
+ /** Convert discord.js message attachments to PlatformAttachment[]. */
197
+ function toAttachments(msg) {
198
+ return [...msg.attachments.values()].map((a) => ({
199
+ name: a.name,
200
+ contentType: a.contentType,
201
+ url: a.url,
202
+ size: a.size,
203
+ }));
204
+ }
205
+ /** Wrap a discord.js Message as a PlatformMessage. */
206
+ function wrapDiscordMessage(message) {
207
+ const author = wrapDiscordUser(message.author);
208
+ const channel = wrapDiscordChannel(message.channel);
209
+ const attachments = toAttachments(message);
210
+ return {
211
+ id: message.id,
212
+ platform: 'discord',
213
+ content: message.content,
214
+ author,
215
+ channel,
216
+ attachments,
217
+ createdAt: message.createdAt,
218
+ async react(emoji) {
219
+ await message.react(emoji);
220
+ },
221
+ async reply(payload) {
222
+ const sent = await message.reply(toDiscordPayload(payload));
223
+ return wrapDiscordSentMessage(sent);
224
+ },
225
+ };
226
+ }
227
+ // ---------------------------------------------------------------------------
228
+ // Interaction wrappers
229
+ // ---------------------------------------------------------------------------
230
+ /** Wrap a discord.js ButtonInteraction as a PlatformButtonInteraction. */
231
+ function wrapDiscordButton(interaction) {
232
+ const user = wrapDiscordUser(interaction.user);
233
+ const channel = interaction.channel
234
+ ? wrapDiscordChannel(interaction.channel)
235
+ : buildFallbackChannel(interaction.channelId);
236
+ return {
237
+ id: interaction.id,
238
+ platform: 'discord',
239
+ customId: interaction.customId,
240
+ user,
241
+ channel,
242
+ messageId: interaction.message.id,
243
+ async deferUpdate() {
244
+ await interaction.deferUpdate();
245
+ },
246
+ async reply(payload) {
247
+ await interaction.reply(toDiscordPayload(payload, EPHEMERAL_ALLOWED));
248
+ },
249
+ async update(payload) {
250
+ await interaction.update(toDiscordPayload(payload, EPHEMERAL_ALLOWED));
251
+ },
252
+ async editReply(payload) {
253
+ await interaction.editReply(toDiscordPayload(payload, EPHEMERAL_ALLOWED));
254
+ },
255
+ async followUp(payload) {
256
+ const sent = await interaction.followUp(toDiscordPayload(payload, EPHEMERAL_ALLOWED));
257
+ return wrapDiscordSentMessage(sent);
258
+ },
259
+ };
260
+ }
261
+ /** Wrap a discord.js StringSelectMenuInteraction as a PlatformSelectInteraction. */
262
+ function wrapDiscordSelect(interaction) {
263
+ const user = wrapDiscordUser(interaction.user);
264
+ const channel = interaction.channel
265
+ ? wrapDiscordChannel(interaction.channel)
266
+ : buildFallbackChannel(interaction.channelId);
267
+ return {
268
+ id: interaction.id,
269
+ platform: 'discord',
270
+ customId: interaction.customId,
271
+ user,
272
+ channel,
273
+ values: interaction.values,
274
+ messageId: interaction.message.id,
275
+ async deferUpdate() {
276
+ await interaction.deferUpdate();
277
+ },
278
+ async reply(payload) {
279
+ await interaction.reply(toDiscordPayload(payload, EPHEMERAL_ALLOWED));
280
+ },
281
+ async update(payload) {
282
+ await interaction.update(toDiscordPayload(payload, EPHEMERAL_ALLOWED));
283
+ },
284
+ async editReply(payload) {
285
+ await interaction.editReply(toDiscordPayload(payload, EPHEMERAL_ALLOWED));
286
+ },
287
+ async followUp(payload) {
288
+ const sent = await interaction.followUp(toDiscordPayload(payload, EPHEMERAL_ALLOWED));
289
+ return wrapDiscordSentMessage(sent);
290
+ },
291
+ };
292
+ }
293
+ /** Extract command options from a ChatInputCommandInteraction into a ReadonlyMap. */
294
+ function extractCommandOptions(interaction) {
295
+ const map = new Map();
296
+ for (const opt of interaction.options.data) {
297
+ if (opt.value !== undefined && opt.value !== null) {
298
+ map.set(opt.name, opt.value);
299
+ }
300
+ }
301
+ return map;
302
+ }
303
+ /** Wrap a discord.js ChatInputCommandInteraction as a PlatformCommandInteraction. */
304
+ function wrapDiscordCommand(interaction) {
305
+ const user = wrapDiscordUser(interaction.user);
306
+ const channel = interaction.channel
307
+ ? wrapDiscordChannel(interaction.channel)
308
+ : buildFallbackChannel(interaction.channelId);
309
+ const options = extractCommandOptions(interaction);
310
+ return {
311
+ id: interaction.id,
312
+ platform: 'discord',
313
+ commandName: interaction.commandName,
314
+ user,
315
+ channel,
316
+ options,
317
+ async deferReply(opts) {
318
+ await interaction.deferReply({ flags: opts?.ephemeral ? discord_js_1.MessageFlags.Ephemeral : undefined });
319
+ },
320
+ async reply(payload) {
321
+ await interaction.reply(toDiscordPayload(payload, EPHEMERAL_ALLOWED));
322
+ },
323
+ async editReply(payload) {
324
+ await interaction.editReply(toDiscordPayload(payload, EPHEMERAL_ALLOWED));
325
+ },
326
+ async followUp(payload) {
327
+ const sent = await interaction.followUp(toDiscordPayload(payload, EPHEMERAL_ALLOWED));
328
+ return wrapDiscordSentMessage(sent);
329
+ },
330
+ };
331
+ }
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.pipe = exports.withImage = exports.withThumbnail = exports.withTimestamp = exports.withFooter = exports.withFields = exports.addField = exports.withColor = exports.withDescription = exports.withTitle = exports.createRichContent = exports.fromPlatformKey = exports.toPlatformKey = void 0;
4
+ var types_1 = require("./types");
5
+ Object.defineProperty(exports, "toPlatformKey", { enumerable: true, get: function () { return types_1.toPlatformKey; } });
6
+ Object.defineProperty(exports, "fromPlatformKey", { enumerable: true, get: function () { return types_1.fromPlatformKey; } });
7
+ var richContentBuilder_1 = require("./richContentBuilder");
8
+ Object.defineProperty(exports, "createRichContent", { enumerable: true, get: function () { return richContentBuilder_1.createRichContent; } });
9
+ Object.defineProperty(exports, "withTitle", { enumerable: true, get: function () { return richContentBuilder_1.withTitle; } });
10
+ Object.defineProperty(exports, "withDescription", { enumerable: true, get: function () { return richContentBuilder_1.withDescription; } });
11
+ Object.defineProperty(exports, "withColor", { enumerable: true, get: function () { return richContentBuilder_1.withColor; } });
12
+ Object.defineProperty(exports, "addField", { enumerable: true, get: function () { return richContentBuilder_1.addField; } });
13
+ Object.defineProperty(exports, "withFields", { enumerable: true, get: function () { return richContentBuilder_1.withFields; } });
14
+ Object.defineProperty(exports, "withFooter", { enumerable: true, get: function () { return richContentBuilder_1.withFooter; } });
15
+ Object.defineProperty(exports, "withTimestamp", { enumerable: true, get: function () { return richContentBuilder_1.withTimestamp; } });
16
+ Object.defineProperty(exports, "withThumbnail", { enumerable: true, get: function () { return richContentBuilder_1.withThumbnail; } });
17
+ Object.defineProperty(exports, "withImage", { enumerable: true, get: function () { return richContentBuilder_1.withImage; } });
18
+ Object.defineProperty(exports, "pipe", { enumerable: true, get: function () { return richContentBuilder_1.pipe; } });
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ /**
3
+ * Immutable builder functions for RichContent.
4
+ *
5
+ * All functions return a new object — never mutate the input.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.createRichContent = createRichContent;
9
+ exports.withTitle = withTitle;
10
+ exports.withDescription = withDescription;
11
+ exports.withColor = withColor;
12
+ exports.addField = addField;
13
+ exports.withFields = withFields;
14
+ exports.withFooter = withFooter;
15
+ exports.withTimestamp = withTimestamp;
16
+ exports.withThumbnail = withThumbnail;
17
+ exports.withImage = withImage;
18
+ exports.pipe = pipe;
19
+ /** Create an empty RichContent. */
20
+ function createRichContent() {
21
+ return {};
22
+ }
23
+ /** Set the title. */
24
+ function withTitle(rc, title) {
25
+ return { ...rc, title };
26
+ }
27
+ /** Set the description. */
28
+ function withDescription(rc, description) {
29
+ return { ...rc, description };
30
+ }
31
+ /** Set the color (numeric, e.g. 0x5865F2). */
32
+ function withColor(rc, color) {
33
+ return { ...rc, color };
34
+ }
35
+ /** Add a field. Existing fields are preserved. */
36
+ function addField(rc, name, value, inline) {
37
+ const field = { name, value, inline };
38
+ const fields = rc.fields ? [...rc.fields, field] : [field];
39
+ return { ...rc, fields };
40
+ }
41
+ /** Replace all fields at once. */
42
+ function withFields(rc, fields) {
43
+ return { ...rc, fields: fields.map((field) => ({ ...field })) };
44
+ }
45
+ /** Set the footer text. */
46
+ function withFooter(rc, footer) {
47
+ return { ...rc, footer };
48
+ }
49
+ /** Set the timestamp. */
50
+ function withTimestamp(rc, timestamp) {
51
+ return { ...rc, timestamp: timestamp ?? new Date() };
52
+ }
53
+ /** Set the thumbnail URL. */
54
+ function withThumbnail(rc, thumbnailUrl) {
55
+ return { ...rc, thumbnailUrl };
56
+ }
57
+ /** Set the image URL. */
58
+ function withImage(rc, imageUrl) {
59
+ return { ...rc, imageUrl };
60
+ }
61
+ // ---------------------------------------------------------------------------
62
+ // Convenience: fluent-style chain helper
63
+ // ---------------------------------------------------------------------------
64
+ /**
65
+ * Apply a sequence of transforms to a RichContent.
66
+ * Each transform is a function `(rc: RichContent) => RichContent`.
67
+ * Usage:
68
+ * pipe(
69
+ * createRichContent(),
70
+ * (rc) => withTitle(rc, 'Hello'),
71
+ * (rc) => withColor(rc, 0x00FF00),
72
+ * )
73
+ */
74
+ function pipe(initial, ...transforms) {
75
+ return transforms.reduce((acc, fn) => fn(acc), initial);
76
+ }
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toTelegramPayload = exports.wrapTelegramSentMessage = exports.wrapTelegramCallbackQuery = exports.wrapTelegramMessage = exports.wrapTelegramChannel = exports.wrapTelegramUser = exports.richContentToHtml = exports.markdownToTelegramHtml = exports.escapeHtml = exports.TelegramAdapter = void 0;
4
+ var telegramAdapter_1 = require("./telegramAdapter");
5
+ Object.defineProperty(exports, "TelegramAdapter", { enumerable: true, get: function () { return telegramAdapter_1.TelegramAdapter; } });
6
+ var telegramFormatter_1 = require("./telegramFormatter");
7
+ Object.defineProperty(exports, "escapeHtml", { enumerable: true, get: function () { return telegramFormatter_1.escapeHtml; } });
8
+ Object.defineProperty(exports, "markdownToTelegramHtml", { enumerable: true, get: function () { return telegramFormatter_1.markdownToTelegramHtml; } });
9
+ Object.defineProperty(exports, "richContentToHtml", { enumerable: true, get: function () { return telegramFormatter_1.richContentToHtml; } });
10
+ var wrappers_1 = require("./wrappers");
11
+ Object.defineProperty(exports, "wrapTelegramUser", { enumerable: true, get: function () { return wrappers_1.wrapTelegramUser; } });
12
+ Object.defineProperty(exports, "wrapTelegramChannel", { enumerable: true, get: function () { return wrappers_1.wrapTelegramChannel; } });
13
+ Object.defineProperty(exports, "wrapTelegramMessage", { enumerable: true, get: function () { return wrappers_1.wrapTelegramMessage; } });
14
+ Object.defineProperty(exports, "wrapTelegramCallbackQuery", { enumerable: true, get: function () { return wrappers_1.wrapTelegramCallbackQuery; } });
15
+ Object.defineProperty(exports, "wrapTelegramSentMessage", { enumerable: true, get: function () { return wrappers_1.wrapTelegramSentMessage; } });
16
+ Object.defineProperty(exports, "toTelegramPayload", { enumerable: true, get: function () { return wrappers_1.toTelegramPayload; } });
@@ -0,0 +1,195 @@
1
+ "use strict";
2
+ /**
3
+ * Telegram platform adapter.
4
+ *
5
+ * Implements the PlatformAdapter interface using a TelegramBotLike instance.
6
+ * This adapter translates Telegram events to the platform-agnostic event model
7
+ * so the bot core can operate without platform-specific knowledge.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.TelegramAdapter = void 0;
11
+ const wrappers_1 = require("./wrappers");
12
+ const logger_1 = require("../../utils/logger");
13
+ // ---------------------------------------------------------------------------
14
+ // TelegramAdapter
15
+ // ---------------------------------------------------------------------------
16
+ class TelegramAdapter {
17
+ platform = 'telegram';
18
+ bot;
19
+ botUserId;
20
+ events = null;
21
+ started = false;
22
+ handlersRegistered = false;
23
+ /** Timestamp when the adapter started — messages older than this are discarded. */
24
+ startedAt = 0;
25
+ constructor(bot, botUserId) {
26
+ this.bot = bot;
27
+ this.botUserId = botUserId;
28
+ }
29
+ /**
30
+ * Start the adapter.
31
+ *
32
+ * Registers Telegram event handlers that translate incoming events to the
33
+ * platform-agnostic event callbacks, then starts the bot polling loop.
34
+ */
35
+ async start(events) {
36
+ if (this.started) {
37
+ throw new Error('TelegramAdapter is already started');
38
+ }
39
+ this.events = events;
40
+ // Round down to whole seconds to match Telegram's second-precision timestamps
41
+ this.startedAt = Math.floor(Date.now() / 1000) * 1000;
42
+ if (!this.handlersRegistered) {
43
+ this.registerHandlers();
44
+ this.handlersRegistered = true;
45
+ }
46
+ // bot.start() returns a Promise that resolves when polling stops.
47
+ // We intentionally do NOT await it (would block forever).
48
+ // Catch errors to prevent unhandled promise rejections (e.g. getMe()
49
+ // failure inside grammY's init phase).
50
+ const startPromise = this.bot.start();
51
+ if (startPromise && typeof startPromise.catch === 'function') {
52
+ startPromise.catch((err) => {
53
+ logger_1.logger.error('[TelegramAdapter] Polling loop error:', err instanceof Error ? err.message : err);
54
+ this.emitError(err);
55
+ });
56
+ }
57
+ this.started = true;
58
+ if (this.events.onReady) {
59
+ this.events.onReady();
60
+ }
61
+ }
62
+ /**
63
+ * Stop the adapter (disconnect, cleanup).
64
+ */
65
+ async stop() {
66
+ if (!this.started)
67
+ return;
68
+ this.bot.stop();
69
+ this.started = false;
70
+ this.events = null;
71
+ }
72
+ /**
73
+ * Retrieve a channel (chat) by its platform-native ID.
74
+ * Returns a PlatformChannel backed by the bot API.
75
+ */
76
+ async getChannel(chatId) {
77
+ try {
78
+ const chat = await this.bot.api.getChat(chatId);
79
+ if (!chat)
80
+ return null;
81
+ const channel = (0, wrappers_1.wrapTelegramChannel)(this.bot.api, chatId, this.bot.toInputFile);
82
+ // Enrich with name from the fetched chat data
83
+ return {
84
+ ...channel,
85
+ name: chat.title ?? chat.first_name ?? undefined,
86
+ };
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ }
92
+ /**
93
+ * Return the bot's own user ID.
94
+ */
95
+ getBotUserId() {
96
+ return this.botUserId;
97
+ }
98
+ // -----------------------------------------------------------------------
99
+ // Private helpers
100
+ // -----------------------------------------------------------------------
101
+ /**
102
+ * Shared handler for both text and photo messages.
103
+ * Wraps the Telegram message and fires onMessage.
104
+ */
105
+ handleIncomingMessage(eventName, ctx) {
106
+ if (!this.events?.onMessage)
107
+ return;
108
+ try {
109
+ const msg = ctx.message ?? ctx.msg;
110
+ if (!msg)
111
+ return;
112
+ const msgTimestampMs = msg.date ? msg.date * 1000 : 0;
113
+ const delayMs = msgTimestampMs ? Date.now() - msgTimestampMs : null;
114
+ logger_1.logger.debug(`[TelegramAdapter] ${eventName} received (chat=${msg.chat.id}, delay=${delayMs !== null ? `${delayMs}ms` : 'unknown'})`);
115
+ // Discard messages sent before the adapter started (stale backlog)
116
+ if (msgTimestampMs && msgTimestampMs < this.startedAt) {
117
+ logger_1.logger.info(`[TelegramAdapter] Ignoring stale message (chat=${msg.chat.id}, age=${Math.round((this.startedAt - msgTimestampMs) / 1000)}s before startup)`);
118
+ return;
119
+ }
120
+ const platformMessage = (0, wrappers_1.wrapTelegramMessage)(msg, this.bot.api, this.bot.toInputFile, this.bot.token);
121
+ // Fire-and-forget: do NOT await so grammY's update loop stays
122
+ // unblocked. This allows /stop and other commands to be received
123
+ // while a long-running response is being monitored.
124
+ // The workspace queue in telegramMessageHandler serializes
125
+ // actual prompt processing per workspace.
126
+ this.events.onMessage(platformMessage).catch((error) => {
127
+ this.emitError(error);
128
+ });
129
+ }
130
+ catch (error) {
131
+ this.emitError(error);
132
+ }
133
+ }
134
+ registerHandlers() {
135
+ // Text messages
136
+ this.bot.on('message:text', async (ctx) => {
137
+ this.handleIncomingMessage('message:text', ctx);
138
+ });
139
+ // Photo messages
140
+ this.bot.on('message:photo', async (ctx) => {
141
+ this.handleIncomingMessage('message:photo', ctx);
142
+ });
143
+ // Callback queries (button presses and select menu selections)
144
+ this.bot.on('callback_query:data', async (ctx) => {
145
+ if (!this.events?.onButtonInteraction && !this.events?.onSelectInteraction)
146
+ return;
147
+ try {
148
+ const query = ctx.callbackQuery;
149
+ if (!query)
150
+ return;
151
+ const interaction = (0, wrappers_1.wrapTelegramCallbackQuery)(query, this.bot.api);
152
+ // Select menu callbacks use \x1F (Unit Separator) between
153
+ // customId and value. Regular button customIds never contain
154
+ // \x1F, so this cleanly distinguishes the two.
155
+ const sepIdx = (query.data ?? '').indexOf(wrappers_1.SELECT_CALLBACK_SEP);
156
+ if (sepIdx > 0 && this.events.onSelectInteraction) {
157
+ const selectCustomId = (query.data ?? '').slice(0, sepIdx);
158
+ const selectedValue = (query.data ?? '').slice(sepIdx + 1);
159
+ await this.events.onSelectInteraction({
160
+ id: query.id,
161
+ platform: 'telegram',
162
+ customId: selectCustomId,
163
+ user: interaction.user,
164
+ channel: interaction.channel,
165
+ values: [selectedValue],
166
+ messageId: interaction.messageId,
167
+ deferUpdate: interaction.deferUpdate,
168
+ reply: interaction.reply,
169
+ update: interaction.update,
170
+ editReply: interaction.editReply,
171
+ followUp: interaction.followUp,
172
+ });
173
+ return;
174
+ }
175
+ if (this.events.onButtonInteraction) {
176
+ await this.events.onButtonInteraction(interaction);
177
+ }
178
+ }
179
+ catch (error) {
180
+ this.emitError(error);
181
+ }
182
+ });
183
+ }
184
+ emitError(error) {
185
+ if (!this.events?.onError)
186
+ return;
187
+ if (error instanceof Error) {
188
+ this.events.onError(error);
189
+ }
190
+ else {
191
+ this.events.onError(new Error(String(error)));
192
+ }
193
+ }
194
+ }
195
+ exports.TelegramAdapter = TelegramAdapter;