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