@pocketping/sdk-node 1.2.0 → 1.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.
package/dist/index.cjs CHANGED
@@ -20,8 +20,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ DiscordBridge: () => DiscordBridge,
23
24
  MemoryStorage: () => MemoryStorage,
24
- PocketPing: () => PocketPing
25
+ PocketPing: () => PocketPing,
26
+ SlackBridge: () => SlackBridge,
27
+ TelegramBridge: () => TelegramBridge,
28
+ WebhookHandler: () => WebhookHandler
25
29
  });
26
30
  module.exports = __toCommonJS(index_exports);
27
31
 
@@ -35,6 +39,7 @@ var MemoryStorage = class {
35
39
  this.sessions = /* @__PURE__ */ new Map();
36
40
  this.messages = /* @__PURE__ */ new Map();
37
41
  this.messageById = /* @__PURE__ */ new Map();
42
+ this.bridgeMessageIds = /* @__PURE__ */ new Map();
38
43
  }
39
44
  async createSession(session) {
40
45
  this.sessions.set(session.id, session);
@@ -79,6 +84,23 @@ var MemoryStorage = class {
79
84
  async getMessage(messageId) {
80
85
  return this.messageById.get(messageId) ?? null;
81
86
  }
87
+ async updateMessage(message) {
88
+ this.messageById.set(message.id, message);
89
+ const sessionMessages = this.messages.get(message.sessionId);
90
+ if (sessionMessages) {
91
+ const index = sessionMessages.findIndex((m) => m.id === message.id);
92
+ if (index !== -1) {
93
+ sessionMessages[index] = message;
94
+ }
95
+ }
96
+ }
97
+ async saveBridgeMessageIds(messageId, bridgeIds) {
98
+ const existing = this.bridgeMessageIds.get(messageId) ?? {};
99
+ this.bridgeMessageIds.set(messageId, { ...existing, ...bridgeIds });
100
+ }
101
+ async getBridgeMessageIds(messageId) {
102
+ return this.bridgeMessageIds.get(messageId) ?? null;
103
+ }
82
104
  async cleanupOldSessions(olderThan) {
83
105
  let count = 0;
84
106
  for (const [id, session] of this.sessions) {
@@ -332,6 +354,12 @@ var PocketPing = class {
332
354
  case "identify":
333
355
  result = await this.handleIdentify(body);
334
356
  break;
357
+ case "edit":
358
+ result = await this.handleEditMessage(body);
359
+ break;
360
+ case "delete":
361
+ result = await this.handleDeleteMessage(body);
362
+ break;
335
363
  default:
336
364
  if (next) {
337
365
  next();
@@ -656,6 +684,99 @@ var PocketPing = class {
656
684
  return this.storage.getSession(sessionId);
657
685
  }
658
686
  // ─────────────────────────────────────────────────────────────────
687
+ // Message Edit/Delete
688
+ // ─────────────────────────────────────────────────────────────────
689
+ /**
690
+ * Handle message edit from widget
691
+ * Only the message sender can edit their own messages
692
+ */
693
+ async handleEditMessage(request) {
694
+ const session = await this.storage.getSession(request.sessionId);
695
+ if (!session) {
696
+ throw new Error("Session not found");
697
+ }
698
+ const message = await this.storage.getMessage(request.messageId);
699
+ if (!message) {
700
+ throw new Error("Message not found");
701
+ }
702
+ if (message.sessionId !== request.sessionId) {
703
+ throw new Error("Message does not belong to this session");
704
+ }
705
+ if (message.deletedAt) {
706
+ throw new Error("Cannot edit deleted message");
707
+ }
708
+ if (message.sender !== "visitor") {
709
+ throw new Error("Cannot edit this message");
710
+ }
711
+ if (!request.content || request.content.trim().length === 0) {
712
+ throw new Error("Content is required");
713
+ }
714
+ if (request.content.length > 4e3) {
715
+ throw new Error("Content exceeds maximum length");
716
+ }
717
+ message.content = request.content.trim();
718
+ message.editedAt = /* @__PURE__ */ new Date();
719
+ if (this.storage.updateMessage) {
720
+ await this.storage.updateMessage(message);
721
+ } else {
722
+ await this.storage.saveMessage(message);
723
+ }
724
+ await this.syncEditToBridges(message.id, message.content);
725
+ this.broadcastToSession(request.sessionId, {
726
+ type: "message_edited",
727
+ data: {
728
+ messageId: message.id,
729
+ content: message.content,
730
+ editedAt: message.editedAt.toISOString()
731
+ }
732
+ });
733
+ return {
734
+ message: {
735
+ id: message.id,
736
+ content: message.content,
737
+ editedAt: message.editedAt.toISOString()
738
+ }
739
+ };
740
+ }
741
+ /**
742
+ * Handle message delete from widget
743
+ * Only the message sender can delete their own messages
744
+ */
745
+ async handleDeleteMessage(request) {
746
+ const session = await this.storage.getSession(request.sessionId);
747
+ if (!session) {
748
+ throw new Error("Session not found");
749
+ }
750
+ const message = await this.storage.getMessage(request.messageId);
751
+ if (!message) {
752
+ throw new Error("Message not found");
753
+ }
754
+ if (message.sessionId !== request.sessionId) {
755
+ throw new Error("Message does not belong to this session");
756
+ }
757
+ if (message.deletedAt) {
758
+ throw new Error("Message already deleted");
759
+ }
760
+ if (message.sender !== "visitor") {
761
+ throw new Error("Cannot delete this message");
762
+ }
763
+ await this.syncDeleteToBridges(message.id);
764
+ message.deletedAt = /* @__PURE__ */ new Date();
765
+ if (this.storage.updateMessage) {
766
+ await this.storage.updateMessage(message);
767
+ } else {
768
+ await this.storage.saveMessage(message);
769
+ }
770
+ this.broadcastToSession(request.sessionId, {
771
+ type: "message_deleted",
772
+ data: {
773
+ messageId: message.id,
774
+ deletedAt: message.deletedAt.toISOString()
775
+ }
776
+ });
777
+ return { deleted: true };
778
+ }
779
+ // ─────────────────────────────────────────────────────────────────
659
780
  // Operator Actions (for bridges)
660
781
  // ─────────────────────────────────────────────────────────────────
661
782
  async sendOperatorMessage(sessionId, content) {
@@ -799,9 +920,23 @@ var PocketPing = class {
799
920
  case "new_session":
800
921
  await bridge.onNewSession?.(args[0]);
801
922
  break;
802
- case "message":
803
- await bridge.onVisitorMessage?.(args[0], args[1]);
923
+ case "message": {
924
+ const message = args[0];
925
+ const session = args[1];
926
+ const result = await bridge.onVisitorMessage?.(message, session);
927
+ if (result?.messageId && this.storage.saveBridgeMessageIds) {
928
+ const bridgeIds = {};
929
+ if (bridge.name === "telegram") {
930
+ bridgeIds.telegramMessageId = result.messageId;
931
+ } else if (bridge.name === "discord") {
932
+ bridgeIds.discordMessageId = result.messageId;
933
+ } else if (bridge.name === "slack") {
934
+ bridgeIds.slackMessageTs = result.messageId;
935
+ }
936
+ await this.storage.saveBridgeMessageIds(message.id, bridgeIds);
937
+ }
804
938
  break;
939
+ }
805
940
  }
806
941
  } catch (err) {
807
942
  console.error(`[PocketPing] Bridge ${bridge.name} error:`, err);
@@ -835,6 +970,66 @@ var PocketPing = class {
835
970
  }
836
971
  }
837
972
  }
973
+ /**
974
+ * Sync message edit to all bridges that support it
975
+ */
976
+ async syncEditToBridges(messageId, newContent) {
977
+ if (!this.storage.getBridgeMessageIds) {
978
+ return;
979
+ }
980
+ const bridgeIds = await this.storage.getBridgeMessageIds(messageId);
981
+ if (!bridgeIds) {
982
+ return;
983
+ }
984
+ for (const bridge of this.bridges) {
985
+ if (!bridge.onMessageEdit) continue;
986
+ try {
987
+ let bridgeMessageId;
988
+ if (bridge.name === "telegram" && bridgeIds.telegramMessageId) {
989
+ bridgeMessageId = bridgeIds.telegramMessageId;
990
+ } else if (bridge.name === "discord" && bridgeIds.discordMessageId) {
991
+ bridgeMessageId = bridgeIds.discordMessageId;
992
+ } else if (bridge.name === "slack" && bridgeIds.slackMessageTs) {
993
+ bridgeMessageId = bridgeIds.slackMessageTs;
994
+ }
995
+ if (bridgeMessageId) {
996
+ await bridge.onMessageEdit(messageId, newContent, bridgeMessageId);
997
+ }
998
+ } catch (err) {
999
+ console.error(`[PocketPing] Bridge ${bridge.name} edit sync error:`, err);
1000
+ }
1001
+ }
1002
+ }
1003
+ /**
1004
+ * Sync message delete to all bridges that support it
1005
+ */
1006
+ async syncDeleteToBridges(messageId) {
1007
+ if (!this.storage.getBridgeMessageIds) {
1008
+ return;
1009
+ }
1010
+ const bridgeIds = await this.storage.getBridgeMessageIds(messageId);
1011
+ if (!bridgeIds) {
1012
+ return;
1013
+ }
1014
+ for (const bridge of this.bridges) {
1015
+ if (!bridge.onMessageDelete) continue;
1016
+ try {
1017
+ let bridgeMessageId;
1018
+ if (bridge.name === "telegram" && bridgeIds.telegramMessageId) {
1019
+ bridgeMessageId = bridgeIds.telegramMessageId;
1020
+ } else if (bridge.name === "discord" && bridgeIds.discordMessageId) {
1021
+ bridgeMessageId = bridgeIds.discordMessageId;
1022
+ } else if (bridge.name === "slack" && bridgeIds.slackMessageTs) {
1023
+ bridgeMessageId = bridgeIds.slackMessageTs;
1024
+ }
1025
+ if (bridgeMessageId) {
1026
+ await bridge.onMessageDelete(messageId, bridgeMessageId);
1027
+ }
1028
+ } catch (err) {
1029
+ console.error(`[PocketPing] Bridge ${bridge.name} delete sync error:`, err);
1030
+ }
1031
+ }
1032
+ }
838
1033
  // ─────────────────────────────────────────────────────────────────
839
1034
  // Webhook Forwarding
840
1035
  // ─────────────────────────────────────────────────────────────────
@@ -994,8 +1189,1250 @@ var PocketPing = class {
994
1189
  });
995
1190
  }
996
1191
  };
1192
+
1193
+ // src/webhooks.ts
1194
+ var DISCORD_INTERACTION_PING = 1;
1195
+ var DISCORD_INTERACTION_APPLICATION_COMMAND = 2;
1196
+ var DISCORD_RESPONSE_PONG = 1;
1197
+ var DISCORD_RESPONSE_CHANNEL_MESSAGE = 4;
1198
+ var WebhookHandler = class {
1199
+ constructor(config) {
1200
+ this.config = config;
1201
+ }
1202
+ /**
1203
+ * Create an Express/Connect middleware for handling Telegram webhooks
1204
+ */
1205
+ handleTelegramWebhook() {
1206
+ return async (req, res) => {
1207
+ if (!this.config.telegramBotToken) {
1208
+ res.statusCode = 404;
1209
+ res.end(JSON.stringify({ error: "Telegram not configured" }));
1210
+ return;
1211
+ }
1212
+ try {
1213
+ const body = await this.parseBody(req);
1214
+ const update = body;
1215
+ if (update.message) {
1216
+ const msg = update.message;
1217
+ if (msg.text?.startsWith("/")) {
1218
+ this.writeOK(res);
1219
+ return;
1220
+ }
1221
+ const text = msg.text ?? msg.caption ?? "";
1222
+ let media = null;
1223
+ if (msg.photo && msg.photo.length > 0) {
1224
+ const largest = msg.photo[msg.photo.length - 1];
1225
+ media = {
1226
+ fileId: largest.file_id,
1227
+ filename: `photo_${Date.now()}.jpg`,
1228
+ mimeType: "image/jpeg",
1229
+ size: largest.file_size ?? 0
1230
+ };
1231
+ } else if (msg.document) {
1232
+ media = {
1233
+ fileId: msg.document.file_id,
1234
+ filename: msg.document.file_name ?? `document_${Date.now()}`,
1235
+ mimeType: msg.document.mime_type ?? "application/octet-stream",
1236
+ size: msg.document.file_size ?? 0
1237
+ };
1238
+ } else if (msg.audio) {
1239
+ media = {
1240
+ fileId: msg.audio.file_id,
1241
+ filename: msg.audio.file_name ?? `audio_${Date.now()}.mp3`,
1242
+ mimeType: msg.audio.mime_type ?? "audio/mpeg",
1243
+ size: msg.audio.file_size ?? 0
1244
+ };
1245
+ } else if (msg.video) {
1246
+ media = {
1247
+ fileId: msg.video.file_id,
1248
+ filename: msg.video.file_name ?? `video_${Date.now()}.mp4`,
1249
+ mimeType: msg.video.mime_type ?? "video/mp4",
1250
+ size: msg.video.file_size ?? 0
1251
+ };
1252
+ } else if (msg.voice) {
1253
+ media = {
1254
+ fileId: msg.voice.file_id,
1255
+ filename: `voice_${Date.now()}.ogg`,
1256
+ mimeType: msg.voice.mime_type ?? "audio/ogg",
1257
+ size: msg.voice.file_size ?? 0
1258
+ };
1259
+ }
1260
+ if (!text && !media) {
1261
+ this.writeOK(res);
1262
+ return;
1263
+ }
1264
+ const topicId = msg.message_thread_id;
1265
+ if (!topicId) {
1266
+ this.writeOK(res);
1267
+ return;
1268
+ }
1269
+ const operatorName = msg.from?.first_name ?? "Operator";
1270
+ const attachments = [];
1271
+ if (media) {
1272
+ const data = await this.downloadTelegramFile(media.fileId);
1273
+ if (data) {
1274
+ attachments.push({
1275
+ filename: media.filename,
1276
+ mimeType: media.mimeType,
1277
+ size: media.size,
1278
+ data,
1279
+ bridgeFileId: media.fileId
1280
+ });
1281
+ }
1282
+ }
1283
+ await this.config.onOperatorMessage(
1284
+ String(topicId),
1285
+ text,
1286
+ operatorName,
1287
+ "telegram",
1288
+ attachments
1289
+ );
1290
+ }
1291
+ this.writeOK(res);
1292
+ } catch (error) {
1293
+ console.error("[WebhookHandler] Telegram error:", error);
1294
+ res.statusCode = 500;
1295
+ res.end(JSON.stringify({ error: "Internal server error" }));
1296
+ }
1297
+ };
1298
+ }
1299
+ /**
1300
+ * Create an Express/Connect middleware for handling Slack webhooks
1301
+ */
1302
+ handleSlackWebhook() {
1303
+ return async (req, res) => {
1304
+ if (!this.config.slackBotToken) {
1305
+ res.statusCode = 404;
1306
+ res.end(JSON.stringify({ error: "Slack not configured" }));
1307
+ return;
1308
+ }
1309
+ try {
1310
+ const body = await this.parseBody(req);
1311
+ const payload = body;
1312
+ if (payload.type === "url_verification" && payload.challenge) {
1313
+ res.setHeader("Content-Type", "application/json");
1314
+ res.end(JSON.stringify({ challenge: payload.challenge }));
1315
+ return;
1316
+ }
1317
+ if (payload.type === "event_callback" && payload.event) {
1318
+ const event = payload.event;
1319
+ const hasContent = event.type === "message" && event.thread_ts && !event.bot_id && !event.subtype;
1320
+ const hasFiles = event.files && event.files.length > 0;
1321
+ if (hasContent && (event.text || hasFiles)) {
1322
+ const threadTs = event.thread_ts;
1323
+ const text = event.text ?? "";
1324
+ const attachments = [];
1325
+ if (hasFiles && event.files) {
1326
+ for (const file of event.files) {
1327
+ const data = await this.downloadSlackFile(file);
1328
+ if (data) {
1329
+ attachments.push({
1330
+ filename: file.name,
1331
+ mimeType: file.mimetype,
1332
+ size: file.size,
1333
+ data,
1334
+ bridgeFileId: file.id
1335
+ });
1336
+ }
1337
+ }
1338
+ }
1339
+ let operatorName = "Operator";
1340
+ if (event.user) {
1341
+ const name = await this.getSlackUserName(event.user);
1342
+ if (name) operatorName = name;
1343
+ }
1344
+ await this.config.onOperatorMessage(
1345
+ threadTs,
1346
+ text,
1347
+ operatorName,
1348
+ "slack",
1349
+ attachments
1350
+ );
1351
+ }
1352
+ }
1353
+ this.writeOK(res);
1354
+ } catch (error) {
1355
+ console.error("[WebhookHandler] Slack error:", error);
1356
+ res.statusCode = 500;
1357
+ res.end(JSON.stringify({ error: "Internal server error" }));
1358
+ }
1359
+ };
1360
+ }
1361
+ /**
1362
+ * Create an Express/Connect middleware for handling Discord webhooks
1363
+ */
1364
+ handleDiscordWebhook() {
1365
+ return async (req, res) => {
1366
+ try {
1367
+ const body = await this.parseBody(req);
1368
+ const interaction = body;
1369
+ if (interaction.type === DISCORD_INTERACTION_PING) {
1370
+ res.setHeader("Content-Type", "application/json");
1371
+ res.end(JSON.stringify({ type: DISCORD_RESPONSE_PONG }));
1372
+ return;
1373
+ }
1374
+ if (interaction.type === DISCORD_INTERACTION_APPLICATION_COMMAND && interaction.data) {
1375
+ if (interaction.data.name === "reply") {
1376
+ const threadId = interaction.channel_id;
1377
+ const content = interaction.data.options?.find(
1378
+ (opt) => opt.name === "message"
1379
+ )?.value;
1380
+ if (threadId && content) {
1381
+ const operatorName = interaction.member?.user?.username ?? interaction.user?.username ?? "Operator";
1382
+ await this.config.onOperatorMessage(
1383
+ threadId,
1384
+ content,
1385
+ operatorName,
1386
+ "discord",
1387
+ []
1388
+ );
1389
+ res.setHeader("Content-Type", "application/json");
1390
+ res.end(
1391
+ JSON.stringify({
1392
+ type: DISCORD_RESPONSE_CHANNEL_MESSAGE,
1393
+ data: { content: "\u2705 Message sent to visitor" }
1394
+ })
1395
+ );
1396
+ return;
1397
+ }
1398
+ }
1399
+ }
1400
+ res.setHeader("Content-Type", "application/json");
1401
+ res.end(JSON.stringify({ type: DISCORD_RESPONSE_PONG }));
1402
+ } catch (error) {
1403
+ console.error("[WebhookHandler] Discord error:", error);
1404
+ res.statusCode = 500;
1405
+ res.end(JSON.stringify({ error: "Internal server error" }));
1406
+ }
1407
+ };
1408
+ }
1409
+ // ─────────────────────────────────────────────────────────────────
1410
+ // Helper Methods
1411
+ // ─────────────────────────────────────────────────────────────────
1412
+ async parseBody(req) {
1413
+ if (req.body) return req.body;
1414
+ return new Promise((resolve, reject) => {
1415
+ let data = "";
1416
+ req.on("data", (chunk) => data += chunk);
1417
+ req.on("end", () => {
1418
+ try {
1419
+ resolve(data ? JSON.parse(data) : {});
1420
+ } catch {
1421
+ reject(new Error("Invalid JSON"));
1422
+ }
1423
+ });
1424
+ req.on("error", reject);
1425
+ });
1426
+ }
1427
+ writeOK(res) {
1428
+ res.setHeader("Content-Type", "application/json");
1429
+ res.end(JSON.stringify({ ok: true }));
1430
+ }
1431
+ async downloadTelegramFile(fileId) {
1432
+ try {
1433
+ const botToken = this.config.telegramBotToken;
1434
+ const getFileUrl = `https://api.telegram.org/bot${botToken}/getFile?file_id=${fileId}`;
1435
+ const getFileResp = await fetch(getFileUrl);
1436
+ const getFileResult = await getFileResp.json();
1437
+ if (!getFileResult.ok || !getFileResult.result?.file_path) {
1438
+ return null;
1439
+ }
1440
+ const downloadUrl = `https://api.telegram.org/file/bot${botToken}/${getFileResult.result.file_path}`;
1441
+ const downloadResp = await fetch(downloadUrl);
1442
+ const arrayBuffer = await downloadResp.arrayBuffer();
1443
+ return Buffer.from(arrayBuffer);
1444
+ } catch (error) {
1445
+ console.error("[WebhookHandler] Telegram file download error:", error);
1446
+ return null;
1447
+ }
1448
+ }
1449
+ async downloadSlackFile(file) {
1450
+ try {
1451
+ const downloadUrl = file.url_private_download ?? file.url_private;
1452
+ const resp = await fetch(downloadUrl, {
1453
+ headers: {
1454
+ Authorization: `Bearer ${this.config.slackBotToken}`
1455
+ }
1456
+ });
1457
+ if (!resp.ok) {
1458
+ return null;
1459
+ }
1460
+ const arrayBuffer = await resp.arrayBuffer();
1461
+ return Buffer.from(arrayBuffer);
1462
+ } catch (error) {
1463
+ console.error("[WebhookHandler] Slack file download error:", error);
1464
+ return null;
1465
+ }
1466
+ }
1467
+ async getSlackUserName(userId) {
1468
+ try {
1469
+ const url = `https://slack.com/api/users.info?user=${userId}`;
1470
+ const resp = await fetch(url, {
1471
+ headers: {
1472
+ Authorization: `Bearer ${this.config.slackBotToken}`
1473
+ }
1474
+ });
1475
+ const result = await resp.json();
1476
+ if (!result.ok) {
1477
+ return null;
1478
+ }
1479
+ return result.user?.real_name ?? result.user?.name ?? null;
1480
+ } catch {
1481
+ return null;
1482
+ }
1483
+ }
1484
+ };
1485
+
1486
+ // src/bridges/telegram.ts
1487
+ var TelegramBridge = class {
1488
+ constructor(botToken, chatId, options = {}) {
1489
+ this.name = "telegram";
1490
+ this.botToken = botToken;
1491
+ this.chatId = chatId;
1492
+ this.parseMode = options.parseMode ?? "HTML";
1493
+ this.disableNotification = options.disableNotification ?? false;
1494
+ this.baseUrl = `https://api.telegram.org/bot${botToken}`;
1495
+ }
1496
+ /**
1497
+ * Initialize the bridge (optional setup)
1498
+ */
1499
+ async init(_pocketping) {
1500
+ try {
1501
+ const response = await fetch(`${this.baseUrl}/getMe`);
1502
+ const data = await response.json();
1503
+ if (!data.ok) {
1504
+ console.error("[TelegramBridge] Invalid bot token:", data.description);
1505
+ }
1506
+ } catch (error) {
1507
+ console.error("[TelegramBridge] Failed to verify bot token:", error);
1508
+ }
1509
+ }
1510
+ /**
1511
+ * Called when a new chat session is created
1512
+ */
1513
+ async onNewSession(session) {
1514
+ const url = session.metadata?.url || "Unknown page";
1515
+ const text = this.formatNewSession(session.visitorId, url);
1516
+ try {
1517
+ await this.sendMessage(text);
1518
+ } catch (error) {
1519
+ console.error("[TelegramBridge] Failed to send new session notification:", error);
1520
+ }
1521
+ }
1522
+ /**
1523
+ * Called when a visitor sends a message.
1524
+ * Returns the Telegram message ID for edit/delete sync.
1525
+ */
1526
+ async onVisitorMessage(message, session) {
1527
+ const text = this.formatVisitorMessage(session.visitorId, message.content);
1528
+ try {
1529
+ const messageId = await this.sendMessage(text);
1530
+ return { messageId };
1531
+ } catch (error) {
1532
+ console.error("[TelegramBridge] Failed to send visitor message:", error);
1533
+ return {};
1534
+ }
1535
+ }
1536
+ /**
1537
+ * Called when an operator sends a message (for cross-bridge sync)
1538
+ */
1539
+ async onOperatorMessage(message, _session, sourceBridge, operatorName) {
1540
+ if (sourceBridge === "telegram") {
1541
+ return;
1542
+ }
1543
+ const name = operatorName || "Operator";
1544
+ const text = this.parseMode === "HTML" ? `<b>${this.escapeHtml(name)}:</b>
1545
+ ${this.escapeHtml(message.content)}` : `*${this.escapeMarkdown(name)}:*
1546
+ ${this.escapeMarkdown(message.content)}`;
1547
+ try {
1548
+ await this.sendMessage(text);
1549
+ } catch (error) {
1550
+ console.error("[TelegramBridge] Failed to send operator message:", error);
1551
+ }
1552
+ }
1553
+ /**
1554
+ * Called when visitor starts/stops typing
1555
+ */
1556
+ async onTyping(sessionId, isTyping) {
1557
+ if (!isTyping) return;
1558
+ try {
1559
+ await this.sendChatAction("typing");
1560
+ } catch (error) {
1561
+ console.error("[TelegramBridge] Failed to send typing action:", error);
1562
+ }
1563
+ }
1564
+ /**
1565
+ * Called when a visitor edits their message.
1566
+ * @returns true if edit succeeded, false otherwise
1567
+ */
1568
+ async onMessageEdit(_messageId, newContent, bridgeMessageId) {
1569
+ try {
1570
+ const response = await fetch(`${this.baseUrl}/editMessageText`, {
1571
+ method: "POST",
1572
+ headers: { "Content-Type": "application/json" },
1573
+ body: JSON.stringify({
1574
+ chat_id: this.chatId,
1575
+ message_id: bridgeMessageId,
1576
+ text: `${newContent}
1577
+
1578
+ <i>(edited)</i>`,
1579
+ parse_mode: this.parseMode
1580
+ })
1581
+ });
1582
+ const data = await response.json();
1583
+ if (!data.ok) {
1584
+ console.error("[TelegramBridge] Edit failed:", data.description);
1585
+ return false;
1586
+ }
1587
+ return true;
1588
+ } catch (error) {
1589
+ console.error("[TelegramBridge] Failed to edit message:", error);
1590
+ return false;
1591
+ }
1592
+ }
1593
+ /**
1594
+ * Called when a visitor deletes their message.
1595
+ * @returns true if delete succeeded, false otherwise
1596
+ */
1597
+ async onMessageDelete(_messageId, bridgeMessageId) {
1598
+ try {
1599
+ const response = await fetch(`${this.baseUrl}/deleteMessage`, {
1600
+ method: "POST",
1601
+ headers: { "Content-Type": "application/json" },
1602
+ body: JSON.stringify({
1603
+ chat_id: this.chatId,
1604
+ message_id: bridgeMessageId
1605
+ })
1606
+ });
1607
+ const data = await response.json();
1608
+ if (!data.ok) {
1609
+ console.error("[TelegramBridge] Delete failed:", data.description);
1610
+ return false;
1611
+ }
1612
+ return true;
1613
+ } catch (error) {
1614
+ console.error("[TelegramBridge] Failed to delete message:", error);
1615
+ return false;
1616
+ }
1617
+ }
1618
+ /**
1619
+ * Called when a custom event is triggered from the widget
1620
+ */
1621
+ async onCustomEvent(event, session) {
1622
+ const dataStr = event.data ? JSON.stringify(event.data, null, 2) : "";
1623
+ const text = this.parseMode === "HTML" ? `<b>Custom Event:</b> ${this.escapeHtml(event.name)}
1624
+ <b>Visitor:</b> ${this.escapeHtml(session.visitorId)}${dataStr ? `
1625
+ <pre>${this.escapeHtml(dataStr)}</pre>` : ""}` : `*Custom Event:* ${this.escapeMarkdown(event.name)}
1626
+ *Visitor:* ${this.escapeMarkdown(session.visitorId)}${dataStr ? `
1627
+ \`\`\`
1628
+ ${dataStr}
1629
+ \`\`\`` : ""}`;
1630
+ try {
1631
+ await this.sendMessage(text);
1632
+ } catch (error) {
1633
+ console.error("[TelegramBridge] Failed to send custom event:", error);
1634
+ }
1635
+ }
1636
+ /**
1637
+ * Called when a user identifies themselves via PocketPing.identify()
1638
+ */
1639
+ async onIdentityUpdate(session) {
1640
+ if (!session.identity) return;
1641
+ const identity = session.identity;
1642
+ let text;
1643
+ if (this.parseMode === "HTML") {
1644
+ text = `<b>User Identified</b>
1645
+ <b>ID:</b> ${this.escapeHtml(identity.id)}
1646
+ ` + (identity.name ? `<b>Name:</b> ${this.escapeHtml(identity.name)}
1647
+ ` : "") + (identity.email ? `<b>Email:</b> ${this.escapeHtml(identity.email)}` : "");
1648
+ } else {
1649
+ text = `*User Identified*
1650
+ *ID:* ${this.escapeMarkdown(identity.id)}
1651
+ ` + (identity.name ? `*Name:* ${this.escapeMarkdown(identity.name)}
1652
+ ` : "") + (identity.email ? `*Email:* ${this.escapeMarkdown(identity.email)}` : "");
1653
+ }
1654
+ try {
1655
+ await this.sendMessage(text.trim());
1656
+ } catch (error) {
1657
+ console.error("[TelegramBridge] Failed to send identity update:", error);
1658
+ }
1659
+ }
1660
+ // ─────────────────────────────────────────────────────────────────
1661
+ // Private helper methods
1662
+ // ─────────────────────────────────────────────────────────────────
1663
+ /**
1664
+ * Send a message to the Telegram chat
1665
+ */
1666
+ async sendMessage(text) {
1667
+ const response = await fetch(`${this.baseUrl}/sendMessage`, {
1668
+ method: "POST",
1669
+ headers: { "Content-Type": "application/json" },
1670
+ body: JSON.stringify({
1671
+ chat_id: this.chatId,
1672
+ text,
1673
+ parse_mode: this.parseMode,
1674
+ disable_notification: this.disableNotification
1675
+ })
1676
+ });
1677
+ const data = await response.json();
1678
+ if (!data.ok) {
1679
+ throw new Error(`Telegram API error: ${data.description}`);
1680
+ }
1681
+ return data.result?.message_id;
1682
+ }
1683
+ /**
1684
+ * Send a chat action (e.g., "typing")
1685
+ */
1686
+ async sendChatAction(action) {
1687
+ await fetch(`${this.baseUrl}/sendChatAction`, {
1688
+ method: "POST",
1689
+ headers: { "Content-Type": "application/json" },
1690
+ body: JSON.stringify({
1691
+ chat_id: this.chatId,
1692
+ action
1693
+ })
1694
+ });
1695
+ }
1696
+ /**
1697
+ * Format new session notification
1698
+ */
1699
+ formatNewSession(visitorId, url) {
1700
+ if (this.parseMode === "HTML") {
1701
+ return `<b>New chat session</b>
1702
+ <b>Visitor:</b> ${this.escapeHtml(visitorId)}
1703
+ <b>Page:</b> ${this.escapeHtml(url)}`;
1704
+ }
1705
+ return `*New chat session*
1706
+ *Visitor:* ${this.escapeMarkdown(visitorId)}
1707
+ *Page:* ${this.escapeMarkdown(url)}`;
1708
+ }
1709
+ /**
1710
+ * Format visitor message
1711
+ */
1712
+ formatVisitorMessage(visitorId, content) {
1713
+ if (this.parseMode === "HTML") {
1714
+ return `<b>${this.escapeHtml(visitorId)}:</b>
1715
+ ${this.escapeHtml(content)}`;
1716
+ }
1717
+ return `*${this.escapeMarkdown(visitorId)}:*
1718
+ ${this.escapeMarkdown(content)}`;
1719
+ }
1720
+ /**
1721
+ * Escape HTML special characters
1722
+ */
1723
+ escapeHtml(text) {
1724
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1725
+ }
1726
+ /**
1727
+ * Escape Markdown special characters
1728
+ */
1729
+ escapeMarkdown(text) {
1730
+ return text.replace(/[_*[\]()~`>#+=|{}.!-]/g, "\\$&");
1731
+ }
1732
+ };
1733
+
1734
+ // src/bridges/discord.ts
1735
+ var DiscordBridge = class _DiscordBridge {
1736
+ constructor(config) {
1737
+ this.name = "discord";
1738
+ this.mode = config.mode;
1739
+ this.webhookUrl = config.webhookUrl;
1740
+ this.botToken = config.botToken;
1741
+ this.channelId = config.channelId;
1742
+ this.username = config.username;
1743
+ this.avatarUrl = config.avatarUrl;
1744
+ }
1745
+ /**
1746
+ * Create a Discord bridge using a webhook URL
1747
+ */
1748
+ static webhook(webhookUrl, options = {}) {
1749
+ return new _DiscordBridge({
1750
+ mode: "webhook",
1751
+ webhookUrl,
1752
+ username: options.username,
1753
+ avatarUrl: options.avatarUrl
1754
+ });
1755
+ }
1756
+ /**
1757
+ * Create a Discord bridge using a bot token
1758
+ */
1759
+ static bot(botToken, channelId, options = {}) {
1760
+ return new _DiscordBridge({
1761
+ mode: "bot",
1762
+ botToken,
1763
+ channelId,
1764
+ username: options.username,
1765
+ avatarUrl: options.avatarUrl
1766
+ });
1767
+ }
1768
+ /**
1769
+ * Initialize the bridge (optional setup)
1770
+ */
1771
+ async init(_pocketping) {
1772
+ if (this.mode === "bot" && this.botToken) {
1773
+ try {
1774
+ const response = await fetch("https://discord.com/api/v10/users/@me", {
1775
+ headers: { Authorization: `Bot ${this.botToken}` }
1776
+ });
1777
+ if (!response.ok) {
1778
+ console.error("[DiscordBridge] Invalid bot token");
1779
+ }
1780
+ } catch (error) {
1781
+ console.error("[DiscordBridge] Failed to verify bot token:", error);
1782
+ }
1783
+ }
1784
+ }
1785
+ /**
1786
+ * Called when a new chat session is created
1787
+ */
1788
+ async onNewSession(session) {
1789
+ const url = session.metadata?.url || "Unknown page";
1790
+ const embed = {
1791
+ title: "New chat session",
1792
+ color: 5793266,
1793
+ // Discord blurple
1794
+ fields: [
1795
+ { name: "Visitor", value: session.visitorId, inline: true },
1796
+ { name: "Page", value: url, inline: false }
1797
+ ],
1798
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1799
+ };
1800
+ try {
1801
+ await this.sendEmbed(embed);
1802
+ } catch (error) {
1803
+ console.error("[DiscordBridge] Failed to send new session notification:", error);
1804
+ }
1805
+ }
1806
+ /**
1807
+ * Called when a visitor sends a message.
1808
+ * Returns the Discord message ID for edit/delete sync.
1809
+ */
1810
+ async onVisitorMessage(message, session) {
1811
+ const embed = {
1812
+ author: {
1813
+ name: session.visitorId,
1814
+ icon_url: this.avatarUrl
1815
+ },
1816
+ description: message.content,
1817
+ color: 5763719,
1818
+ // Green
1819
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1820
+ };
1821
+ try {
1822
+ const messageId = await this.sendEmbed(embed);
1823
+ return { messageId };
1824
+ } catch (error) {
1825
+ console.error("[DiscordBridge] Failed to send visitor message:", error);
1826
+ return {};
1827
+ }
1828
+ }
1829
+ /**
1830
+ * Called when an operator sends a message (for cross-bridge sync)
1831
+ */
1832
+ async onOperatorMessage(message, _session, sourceBridge, operatorName) {
1833
+ if (sourceBridge === "discord") {
1834
+ return;
1835
+ }
1836
+ const embed = {
1837
+ author: {
1838
+ name: operatorName || "Operator",
1839
+ icon_url: this.avatarUrl
1840
+ },
1841
+ description: message.content,
1842
+ color: 16705372,
1843
+ // Yellow
1844
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1845
+ };
1846
+ try {
1847
+ await this.sendEmbed(embed);
1848
+ } catch (error) {
1849
+ console.error("[DiscordBridge] Failed to send operator message:", error);
1850
+ }
1851
+ }
1852
+ /**
1853
+ * Called when visitor starts/stops typing
1854
+ */
1855
+ async onTyping(_sessionId, isTyping) {
1856
+ if (!isTyping || this.mode !== "bot" || !this.channelId) return;
1857
+ try {
1858
+ await fetch(
1859
+ `https://discord.com/api/v10/channels/${this.channelId}/typing`,
1860
+ {
1861
+ method: "POST",
1862
+ headers: { Authorization: `Bot ${this.botToken}` }
1863
+ }
1864
+ );
1865
+ } catch (error) {
1866
+ console.error("[DiscordBridge] Failed to send typing indicator:", error);
1867
+ }
1868
+ }
1869
+ /**
1870
+ * Called when a visitor edits their message.
1871
+ * @returns true if edit succeeded, false otherwise
1872
+ */
1873
+ async onMessageEdit(_messageId, newContent, bridgeMessageId) {
1874
+ try {
1875
+ if (this.mode === "webhook" && this.webhookUrl) {
1876
+ const response = await fetch(
1877
+ `${this.webhookUrl}/messages/${bridgeMessageId}`,
1878
+ {
1879
+ method: "PATCH",
1880
+ headers: { "Content-Type": "application/json" },
1881
+ body: JSON.stringify({
1882
+ embeds: [
1883
+ {
1884
+ description: `${newContent}
1885
+
1886
+ *(edited)*`,
1887
+ color: 5763719
1888
+ }
1889
+ ]
1890
+ })
1891
+ }
1892
+ );
1893
+ return response.ok;
1894
+ } else if (this.mode === "bot" && this.channelId) {
1895
+ const response = await fetch(
1896
+ `https://discord.com/api/v10/channels/${this.channelId}/messages/${bridgeMessageId}`,
1897
+ {
1898
+ method: "PATCH",
1899
+ headers: {
1900
+ "Content-Type": "application/json",
1901
+ Authorization: `Bot ${this.botToken}`
1902
+ },
1903
+ body: JSON.stringify({
1904
+ embeds: [
1905
+ {
1906
+ description: `${newContent}
1907
+
1908
+ *(edited)*`,
1909
+ color: 5763719
1910
+ }
1911
+ ]
1912
+ })
1913
+ }
1914
+ );
1915
+ return response.ok;
1916
+ }
1917
+ return false;
1918
+ } catch (error) {
1919
+ console.error("[DiscordBridge] Failed to edit message:", error);
1920
+ return false;
1921
+ }
1922
+ }
1923
+ /**
1924
+ * Called when a visitor deletes their message.
1925
+ * @returns true if delete succeeded, false otherwise
1926
+ */
1927
+ async onMessageDelete(_messageId, bridgeMessageId) {
1928
+ try {
1929
+ if (this.mode === "webhook" && this.webhookUrl) {
1930
+ const response = await fetch(
1931
+ `${this.webhookUrl}/messages/${bridgeMessageId}`,
1932
+ { method: "DELETE" }
1933
+ );
1934
+ return response.ok || response.status === 404;
1935
+ } else if (this.mode === "bot" && this.channelId) {
1936
+ const response = await fetch(
1937
+ `https://discord.com/api/v10/channels/${this.channelId}/messages/${bridgeMessageId}`,
1938
+ {
1939
+ method: "DELETE",
1940
+ headers: { Authorization: `Bot ${this.botToken}` }
1941
+ }
1942
+ );
1943
+ return response.ok || response.status === 404;
1944
+ }
1945
+ return false;
1946
+ } catch (error) {
1947
+ console.error("[DiscordBridge] Failed to delete message:", error);
1948
+ return false;
1949
+ }
1950
+ }
1951
+ /**
1952
+ * Called when a custom event is triggered from the widget
1953
+ */
1954
+ async onCustomEvent(event, session) {
1955
+ const embed = {
1956
+ title: `Custom Event: ${event.name}`,
1957
+ color: 15418782,
1958
+ // Fuchsia
1959
+ fields: [
1960
+ { name: "Visitor", value: session.visitorId, inline: true },
1961
+ ...event.data ? [
1962
+ {
1963
+ name: "Data",
1964
+ value: `\`\`\`json
1965
+ ${JSON.stringify(event.data, null, 2)}
1966
+ \`\`\``,
1967
+ inline: false
1968
+ }
1969
+ ] : []
1970
+ ],
1971
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1972
+ };
1973
+ try {
1974
+ await this.sendEmbed(embed);
1975
+ } catch (error) {
1976
+ console.error("[DiscordBridge] Failed to send custom event:", error);
1977
+ }
1978
+ }
1979
+ /**
1980
+ * Called when a user identifies themselves via PocketPing.identify()
1981
+ */
1982
+ async onIdentityUpdate(session) {
1983
+ if (!session.identity) return;
1984
+ const identity = session.identity;
1985
+ const fields = [
1986
+ { name: "User ID", value: identity.id, inline: true }
1987
+ ];
1988
+ if (identity.name) {
1989
+ fields.push({ name: "Name", value: identity.name, inline: true });
1990
+ }
1991
+ if (identity.email) {
1992
+ fields.push({ name: "Email", value: identity.email, inline: true });
1993
+ }
1994
+ const embed = {
1995
+ title: "User Identified",
1996
+ color: 5793266,
1997
+ fields,
1998
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1999
+ };
2000
+ try {
2001
+ await this.sendEmbed(embed);
2002
+ } catch (error) {
2003
+ console.error("[DiscordBridge] Failed to send identity update:", error);
2004
+ }
2005
+ }
2006
+ // ─────────────────────────────────────────────────────────────────
2007
+ // Private helper methods
2008
+ // ─────────────────────────────────────────────────────────────────
2009
+ /**
2010
+ * Send an embed to Discord
2011
+ */
2012
+ async sendEmbed(embed) {
2013
+ const body = {
2014
+ embeds: [embed]
2015
+ };
2016
+ if (this.username) {
2017
+ body.username = this.username;
2018
+ }
2019
+ if (this.avatarUrl) {
2020
+ body.avatar_url = this.avatarUrl;
2021
+ }
2022
+ if (this.mode === "webhook" && this.webhookUrl) {
2023
+ const response = await fetch(`${this.webhookUrl}?wait=true`, {
2024
+ method: "POST",
2025
+ headers: { "Content-Type": "application/json" },
2026
+ body: JSON.stringify(body)
2027
+ });
2028
+ if (!response.ok) {
2029
+ const error = await response.text();
2030
+ throw new Error(`Discord webhook error: ${error}`);
2031
+ }
2032
+ const data = await response.json();
2033
+ return data.id;
2034
+ } else if (this.mode === "bot" && this.channelId) {
2035
+ const response = await fetch(
2036
+ `https://discord.com/api/v10/channels/${this.channelId}/messages`,
2037
+ {
2038
+ method: "POST",
2039
+ headers: {
2040
+ "Content-Type": "application/json",
2041
+ Authorization: `Bot ${this.botToken}`
2042
+ },
2043
+ body: JSON.stringify(body)
2044
+ }
2045
+ );
2046
+ if (!response.ok) {
2047
+ const error = await response.text();
2048
+ throw new Error(`Discord API error: ${error}`);
2049
+ }
2050
+ const data = await response.json();
2051
+ return data.id;
2052
+ }
2053
+ return void 0;
2054
+ }
2055
+ };
2056
+
2057
+ // src/bridges/slack.ts
2058
+ var SlackBridge = class _SlackBridge {
2059
+ constructor(config) {
2060
+ this.name = "slack";
2061
+ this.mode = config.mode;
2062
+ this.webhookUrl = config.webhookUrl;
2063
+ this.botToken = config.botToken;
2064
+ this.channelId = config.channelId;
2065
+ this.username = config.username;
2066
+ this.iconEmoji = config.iconEmoji;
2067
+ this.iconUrl = config.iconUrl;
2068
+ }
2069
+ /**
2070
+ * Create a Slack bridge using a webhook URL
2071
+ */
2072
+ static webhook(webhookUrl, options = {}) {
2073
+ return new _SlackBridge({
2074
+ mode: "webhook",
2075
+ webhookUrl,
2076
+ username: options.username,
2077
+ iconEmoji: options.iconEmoji,
2078
+ iconUrl: options.iconUrl
2079
+ });
2080
+ }
2081
+ /**
2082
+ * Create a Slack bridge using a bot token
2083
+ */
2084
+ static bot(botToken, channelId, options = {}) {
2085
+ return new _SlackBridge({
2086
+ mode: "bot",
2087
+ botToken,
2088
+ channelId,
2089
+ username: options.username,
2090
+ iconEmoji: options.iconEmoji,
2091
+ iconUrl: options.iconUrl
2092
+ });
2093
+ }
2094
+ /**
2095
+ * Initialize the bridge (optional setup)
2096
+ */
2097
+ async init(_pocketping) {
2098
+ if (this.mode === "bot" && this.botToken) {
2099
+ try {
2100
+ const response = await fetch("https://slack.com/api/auth.test", {
2101
+ method: "POST",
2102
+ headers: {
2103
+ "Content-Type": "application/json",
2104
+ Authorization: `Bearer ${this.botToken}`
2105
+ }
2106
+ });
2107
+ const data = await response.json();
2108
+ if (!data.ok) {
2109
+ console.error("[SlackBridge] Invalid bot token:", data.error);
2110
+ }
2111
+ } catch (error) {
2112
+ console.error("[SlackBridge] Failed to verify bot token:", error);
2113
+ }
2114
+ }
2115
+ }
2116
+ /**
2117
+ * Called when a new chat session is created
2118
+ */
2119
+ async onNewSession(session) {
2120
+ const url = session.metadata?.url || "Unknown page";
2121
+ const blocks = [
2122
+ {
2123
+ type: "header",
2124
+ text: {
2125
+ type: "plain_text",
2126
+ text: "New chat session",
2127
+ emoji: true
2128
+ }
2129
+ },
2130
+ {
2131
+ type: "section",
2132
+ fields: [
2133
+ {
2134
+ type: "mrkdwn",
2135
+ text: `*Visitor:*
2136
+ ${session.visitorId}`
2137
+ },
2138
+ {
2139
+ type: "mrkdwn",
2140
+ text: `*Page:*
2141
+ ${url}`
2142
+ }
2143
+ ]
2144
+ }
2145
+ ];
2146
+ try {
2147
+ await this.sendBlocks(blocks);
2148
+ } catch (error) {
2149
+ console.error("[SlackBridge] Failed to send new session notification:", error);
2150
+ }
2151
+ }
2152
+ /**
2153
+ * Called when a visitor sends a message.
2154
+ * Returns the Slack message timestamp for edit/delete sync.
2155
+ */
2156
+ async onVisitorMessage(message, session) {
2157
+ const blocks = [
2158
+ {
2159
+ type: "section",
2160
+ text: {
2161
+ type: "mrkdwn",
2162
+ text: `*${this.escapeSlack(session.visitorId)}:*
2163
+ ${this.escapeSlack(message.content)}`
2164
+ }
2165
+ },
2166
+ {
2167
+ type: "context",
2168
+ elements: [
2169
+ {
2170
+ type: "mrkdwn",
2171
+ text: `<!date^${Math.floor(Date.now() / 1e3)}^{date_short_pretty} at {time}|${(/* @__PURE__ */ new Date()).toISOString()}>`
2172
+ }
2173
+ ]
2174
+ }
2175
+ ];
2176
+ try {
2177
+ const messageId = await this.sendBlocks(blocks);
2178
+ return { messageId };
2179
+ } catch (error) {
2180
+ console.error("[SlackBridge] Failed to send visitor message:", error);
2181
+ return {};
2182
+ }
2183
+ }
2184
+ /**
2185
+ * Called when an operator sends a message (for cross-bridge sync)
2186
+ */
2187
+ async onOperatorMessage(message, _session, sourceBridge, operatorName) {
2188
+ if (sourceBridge === "slack") {
2189
+ return;
2190
+ }
2191
+ const name = operatorName || "Operator";
2192
+ const blocks = [
2193
+ {
2194
+ type: "section",
2195
+ text: {
2196
+ type: "mrkdwn",
2197
+ text: `*${this.escapeSlack(name)}:*
2198
+ ${this.escapeSlack(message.content)}`
2199
+ }
2200
+ }
2201
+ ];
2202
+ try {
2203
+ await this.sendBlocks(blocks);
2204
+ } catch (error) {
2205
+ console.error("[SlackBridge] Failed to send operator message:", error);
2206
+ }
2207
+ }
2208
+ /**
2209
+ * Called when visitor starts/stops typing
2210
+ */
2211
+ async onTyping(_sessionId, _isTyping) {
2212
+ }
2213
+ /**
2214
+ * Called when a visitor edits their message.
2215
+ * @returns true if edit succeeded, false otherwise
2216
+ */
2217
+ async onMessageEdit(_messageId, newContent, bridgeMessageId) {
2218
+ if (this.mode !== "bot" || !this.channelId) {
2219
+ console.warn("[SlackBridge] Message edit only supported in bot mode");
2220
+ return false;
2221
+ }
2222
+ try {
2223
+ const response = await fetch("https://slack.com/api/chat.update", {
2224
+ method: "POST",
2225
+ headers: {
2226
+ "Content-Type": "application/json",
2227
+ Authorization: `Bearer ${this.botToken}`
2228
+ },
2229
+ body: JSON.stringify({
2230
+ channel: this.channelId,
2231
+ ts: bridgeMessageId,
2232
+ blocks: [
2233
+ {
2234
+ type: "section",
2235
+ text: {
2236
+ type: "mrkdwn",
2237
+ text: `${this.escapeSlack(newContent)}
2238
+
2239
+ _(edited)_`
2240
+ }
2241
+ }
2242
+ ]
2243
+ })
2244
+ });
2245
+ const data = await response.json();
2246
+ if (!data.ok) {
2247
+ console.error("[SlackBridge] Edit failed:", data.error);
2248
+ return false;
2249
+ }
2250
+ return true;
2251
+ } catch (error) {
2252
+ console.error("[SlackBridge] Failed to edit message:", error);
2253
+ return false;
2254
+ }
2255
+ }
2256
+ /**
2257
+ * Called when a visitor deletes their message.
2258
+ * @returns true if delete succeeded, false otherwise
2259
+ */
2260
+ async onMessageDelete(_messageId, bridgeMessageId) {
2261
+ if (this.mode !== "bot" || !this.channelId) {
2262
+ console.warn("[SlackBridge] Message delete only supported in bot mode");
2263
+ return false;
2264
+ }
2265
+ try {
2266
+ const response = await fetch("https://slack.com/api/chat.delete", {
2267
+ method: "POST",
2268
+ headers: {
2269
+ "Content-Type": "application/json",
2270
+ Authorization: `Bearer ${this.botToken}`
2271
+ },
2272
+ body: JSON.stringify({
2273
+ channel: this.channelId,
2274
+ ts: bridgeMessageId
2275
+ })
2276
+ });
2277
+ const data = await response.json();
2278
+ if (!data.ok) {
2279
+ if (data.error === "message_not_found") {
2280
+ return true;
2281
+ }
2282
+ console.error("[SlackBridge] Delete failed:", data.error);
2283
+ return false;
2284
+ }
2285
+ return true;
2286
+ } catch (error) {
2287
+ console.error("[SlackBridge] Failed to delete message:", error);
2288
+ return false;
2289
+ }
2290
+ }
2291
+ /**
2292
+ * Called when a custom event is triggered from the widget
2293
+ */
2294
+ async onCustomEvent(event, session) {
2295
+ const blocks = [
2296
+ {
2297
+ type: "header",
2298
+ text: {
2299
+ type: "plain_text",
2300
+ text: `Custom Event: ${event.name}`,
2301
+ emoji: true
2302
+ }
2303
+ },
2304
+ {
2305
+ type: "section",
2306
+ fields: [
2307
+ {
2308
+ type: "mrkdwn",
2309
+ text: `*Visitor:*
2310
+ ${session.visitorId}`
2311
+ }
2312
+ ]
2313
+ }
2314
+ ];
2315
+ if (event.data) {
2316
+ blocks.push({
2317
+ type: "section",
2318
+ text: {
2319
+ type: "mrkdwn",
2320
+ text: `*Data:*
2321
+ \`\`\`${JSON.stringify(event.data, null, 2)}\`\`\``
2322
+ }
2323
+ });
2324
+ }
2325
+ try {
2326
+ await this.sendBlocks(blocks);
2327
+ } catch (error) {
2328
+ console.error("[SlackBridge] Failed to send custom event:", error);
2329
+ }
2330
+ }
2331
+ /**
2332
+ * Called when a user identifies themselves via PocketPing.identify()
2333
+ */
2334
+ async onIdentityUpdate(session) {
2335
+ if (!session.identity) return;
2336
+ const identity = session.identity;
2337
+ const fields = [
2338
+ {
2339
+ type: "mrkdwn",
2340
+ text: `*User ID:*
2341
+ ${identity.id}`
2342
+ }
2343
+ ];
2344
+ if (identity.name) {
2345
+ fields.push({
2346
+ type: "mrkdwn",
2347
+ text: `*Name:*
2348
+ ${identity.name}`
2349
+ });
2350
+ }
2351
+ if (identity.email) {
2352
+ fields.push({
2353
+ type: "mrkdwn",
2354
+ text: `*Email:*
2355
+ ${identity.email}`
2356
+ });
2357
+ }
2358
+ const blocks = [
2359
+ {
2360
+ type: "header",
2361
+ text: {
2362
+ type: "plain_text",
2363
+ text: "User Identified",
2364
+ emoji: true
2365
+ }
2366
+ },
2367
+ {
2368
+ type: "section",
2369
+ fields
2370
+ }
2371
+ ];
2372
+ try {
2373
+ await this.sendBlocks(blocks);
2374
+ } catch (error) {
2375
+ console.error("[SlackBridge] Failed to send identity update:", error);
2376
+ }
2377
+ }
2378
+ // ─────────────────────────────────────────────────────────────────
2379
+ // Private helper methods
2380
+ // ─────────────────────────────────────────────────────────────────
2381
+ /**
2382
+ * Send blocks to Slack
2383
+ */
2384
+ async sendBlocks(blocks) {
2385
+ const payload = { blocks };
2386
+ if (this.username) {
2387
+ payload.username = this.username;
2388
+ }
2389
+ if (this.iconUrl) {
2390
+ payload.icon_url = this.iconUrl;
2391
+ } else if (this.iconEmoji) {
2392
+ payload.icon_emoji = this.iconEmoji;
2393
+ }
2394
+ if (this.mode === "webhook" && this.webhookUrl) {
2395
+ const response = await fetch(this.webhookUrl, {
2396
+ method: "POST",
2397
+ headers: { "Content-Type": "application/json" },
2398
+ body: JSON.stringify(payload)
2399
+ });
2400
+ if (!response.ok) {
2401
+ const error = await response.text();
2402
+ throw new Error(`Slack webhook error: ${error}`);
2403
+ }
2404
+ return void 0;
2405
+ } else if (this.mode === "bot" && this.channelId) {
2406
+ payload.channel = this.channelId;
2407
+ const response = await fetch("https://slack.com/api/chat.postMessage", {
2408
+ method: "POST",
2409
+ headers: {
2410
+ "Content-Type": "application/json",
2411
+ Authorization: `Bearer ${this.botToken}`
2412
+ },
2413
+ body: JSON.stringify(payload)
2414
+ });
2415
+ const data = await response.json();
2416
+ if (!data.ok) {
2417
+ throw new Error(`Slack API error: ${data.error}`);
2418
+ }
2419
+ return data.ts;
2420
+ }
2421
+ return void 0;
2422
+ }
2423
+ /**
2424
+ * Escape special characters for Slack mrkdwn
2425
+ */
2426
+ escapeSlack(text) {
2427
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2428
+ }
2429
+ };
997
2430
  // Annotate the CommonJS export names for ESM import in node:
998
2431
  0 && (module.exports = {
2432
+ DiscordBridge,
999
2433
  MemoryStorage,
1000
- PocketPing
2434
+ PocketPing,
2435
+ SlackBridge,
2436
+ TelegramBridge,
2437
+ WebhookHandler
1001
2438
  });