@pocketping/sdk-node 1.2.0 → 1.4.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,1381 @@ 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.edited_message) {
1216
+ const msg = update.edited_message;
1217
+ if (msg.text?.startsWith("/")) {
1218
+ this.writeOK(res);
1219
+ return;
1220
+ }
1221
+ const text = msg.text ?? msg.caption ?? "";
1222
+ if (!text) {
1223
+ this.writeOK(res);
1224
+ return;
1225
+ }
1226
+ const topicId = msg.message_thread_id;
1227
+ if (!topicId) {
1228
+ this.writeOK(res);
1229
+ return;
1230
+ }
1231
+ if (this.config.onOperatorMessageEdit) {
1232
+ const editedAt = msg.edit_date ? new Date(msg.edit_date * 1e3).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
1233
+ await this.config.onOperatorMessageEdit(
1234
+ String(topicId),
1235
+ msg.message_id,
1236
+ text,
1237
+ "telegram",
1238
+ editedAt
1239
+ );
1240
+ }
1241
+ this.writeOK(res);
1242
+ return;
1243
+ }
1244
+ if (update.message) {
1245
+ const msg = update.message;
1246
+ if (msg.text?.startsWith("/")) {
1247
+ this.writeOK(res);
1248
+ return;
1249
+ }
1250
+ const text = msg.text ?? msg.caption ?? "";
1251
+ let media = null;
1252
+ if (msg.photo && msg.photo.length > 0) {
1253
+ const largest = msg.photo[msg.photo.length - 1];
1254
+ media = {
1255
+ fileId: largest.file_id,
1256
+ filename: `photo_${Date.now()}.jpg`,
1257
+ mimeType: "image/jpeg",
1258
+ size: largest.file_size ?? 0
1259
+ };
1260
+ } else if (msg.document) {
1261
+ media = {
1262
+ fileId: msg.document.file_id,
1263
+ filename: msg.document.file_name ?? `document_${Date.now()}`,
1264
+ mimeType: msg.document.mime_type ?? "application/octet-stream",
1265
+ size: msg.document.file_size ?? 0
1266
+ };
1267
+ } else if (msg.audio) {
1268
+ media = {
1269
+ fileId: msg.audio.file_id,
1270
+ filename: msg.audio.file_name ?? `audio_${Date.now()}.mp3`,
1271
+ mimeType: msg.audio.mime_type ?? "audio/mpeg",
1272
+ size: msg.audio.file_size ?? 0
1273
+ };
1274
+ } else if (msg.video) {
1275
+ media = {
1276
+ fileId: msg.video.file_id,
1277
+ filename: msg.video.file_name ?? `video_${Date.now()}.mp4`,
1278
+ mimeType: msg.video.mime_type ?? "video/mp4",
1279
+ size: msg.video.file_size ?? 0
1280
+ };
1281
+ } else if (msg.voice) {
1282
+ media = {
1283
+ fileId: msg.voice.file_id,
1284
+ filename: `voice_${Date.now()}.ogg`,
1285
+ mimeType: msg.voice.mime_type ?? "audio/ogg",
1286
+ size: msg.voice.file_size ?? 0
1287
+ };
1288
+ }
1289
+ if (!text && !media) {
1290
+ this.writeOK(res);
1291
+ return;
1292
+ }
1293
+ const topicId = msg.message_thread_id;
1294
+ if (!topicId) {
1295
+ this.writeOK(res);
1296
+ return;
1297
+ }
1298
+ const operatorName = msg.from?.first_name ?? "Operator";
1299
+ const replyToBridgeMessageId = msg.reply_to_message?.message_id ?? null;
1300
+ const attachments = [];
1301
+ if (media) {
1302
+ const data = await this.downloadTelegramFile(media.fileId);
1303
+ if (data) {
1304
+ attachments.push({
1305
+ filename: media.filename,
1306
+ mimeType: media.mimeType,
1307
+ size: media.size,
1308
+ data,
1309
+ bridgeFileId: media.fileId
1310
+ });
1311
+ }
1312
+ }
1313
+ await this.config.onOperatorMessage(
1314
+ String(topicId),
1315
+ text,
1316
+ operatorName,
1317
+ "telegram",
1318
+ attachments,
1319
+ replyToBridgeMessageId,
1320
+ msg.message_id
1321
+ );
1322
+ }
1323
+ this.writeOK(res);
1324
+ } catch (error) {
1325
+ console.error("[WebhookHandler] Telegram error:", error);
1326
+ res.statusCode = 500;
1327
+ res.end(JSON.stringify({ error: "Internal server error" }));
1328
+ }
1329
+ };
1330
+ }
1331
+ /**
1332
+ * Create an Express/Connect middleware for handling Slack webhooks
1333
+ */
1334
+ handleSlackWebhook() {
1335
+ return async (req, res) => {
1336
+ if (!this.config.slackBotToken) {
1337
+ res.statusCode = 404;
1338
+ res.end(JSON.stringify({ error: "Slack not configured" }));
1339
+ return;
1340
+ }
1341
+ try {
1342
+ const body = await this.parseBody(req);
1343
+ const payload = body;
1344
+ if (payload.type === "url_verification" && payload.challenge) {
1345
+ res.setHeader("Content-Type", "application/json");
1346
+ res.end(JSON.stringify({ challenge: payload.challenge }));
1347
+ return;
1348
+ }
1349
+ if (payload.type === "event_callback" && payload.event) {
1350
+ const event = payload.event;
1351
+ const isAllowedBot = (botId) => !!botId && (this.config.allowedBotIds?.includes(botId) ?? false);
1352
+ if (event.subtype === "message_changed") {
1353
+ if (!this.config.onOperatorMessageEdit) {
1354
+ this.writeOK(res);
1355
+ return;
1356
+ }
1357
+ const botId = event.message?.bot_id ?? event.previous_message?.bot_id ?? event.bot_id;
1358
+ if (botId && !isAllowedBot(botId)) {
1359
+ this.writeOK(res);
1360
+ return;
1361
+ }
1362
+ const threadTs = event.message?.thread_ts ?? event.previous_message?.thread_ts;
1363
+ const messageTs = event.message?.ts ?? event.previous_message?.ts;
1364
+ const text = event.message?.text ?? "";
1365
+ if (threadTs && messageTs) {
1366
+ await this.config.onOperatorMessageEdit(
1367
+ threadTs,
1368
+ messageTs,
1369
+ text,
1370
+ "slack",
1371
+ (/* @__PURE__ */ new Date()).toISOString()
1372
+ );
1373
+ }
1374
+ this.writeOK(res);
1375
+ return;
1376
+ }
1377
+ if (event.subtype === "message_deleted") {
1378
+ if (!this.config.onOperatorMessageDelete) {
1379
+ this.writeOK(res);
1380
+ return;
1381
+ }
1382
+ const botId = event.previous_message?.bot_id ?? event.bot_id;
1383
+ if (botId && !isAllowedBot(botId)) {
1384
+ this.writeOK(res);
1385
+ return;
1386
+ }
1387
+ const threadTs = event.previous_message?.thread_ts;
1388
+ const messageTs = event.deleted_ts ?? event.previous_message?.ts;
1389
+ if (threadTs && messageTs) {
1390
+ await this.config.onOperatorMessageDelete(
1391
+ threadTs,
1392
+ messageTs,
1393
+ "slack",
1394
+ (/* @__PURE__ */ new Date()).toISOString()
1395
+ );
1396
+ }
1397
+ this.writeOK(res);
1398
+ return;
1399
+ }
1400
+ const hasContent = event.type === "message" && event.thread_ts && (!event.bot_id || isAllowedBot(event.bot_id)) && !event.subtype;
1401
+ const hasFiles = event.files && event.files.length > 0;
1402
+ if (hasContent && (event.text || hasFiles)) {
1403
+ const threadTs = event.thread_ts;
1404
+ const text = event.text ?? "";
1405
+ const attachments = [];
1406
+ if (hasFiles && event.files) {
1407
+ for (const file of event.files) {
1408
+ const data = await this.downloadSlackFile(file);
1409
+ if (data) {
1410
+ attachments.push({
1411
+ filename: file.name,
1412
+ mimeType: file.mimetype,
1413
+ size: file.size,
1414
+ data,
1415
+ bridgeFileId: file.id
1416
+ });
1417
+ }
1418
+ }
1419
+ }
1420
+ let operatorName = "Operator";
1421
+ if (event.user) {
1422
+ const name = await this.getSlackUserName(event.user);
1423
+ if (name) operatorName = name;
1424
+ }
1425
+ await this.config.onOperatorMessage(
1426
+ threadTs,
1427
+ text,
1428
+ operatorName,
1429
+ "slack",
1430
+ attachments,
1431
+ null,
1432
+ event.ts ?? null
1433
+ );
1434
+ }
1435
+ }
1436
+ this.writeOK(res);
1437
+ } catch (error) {
1438
+ console.error("[WebhookHandler] Slack error:", error);
1439
+ res.statusCode = 500;
1440
+ res.end(JSON.stringify({ error: "Internal server error" }));
1441
+ }
1442
+ };
1443
+ }
1444
+ /**
1445
+ * Create an Express/Connect middleware for handling Discord webhooks
1446
+ */
1447
+ handleDiscordWebhook() {
1448
+ return async (req, res) => {
1449
+ try {
1450
+ const body = await this.parseBody(req);
1451
+ const interaction = body;
1452
+ if (interaction.type === DISCORD_INTERACTION_PING) {
1453
+ res.setHeader("Content-Type", "application/json");
1454
+ res.end(JSON.stringify({ type: DISCORD_RESPONSE_PONG }));
1455
+ return;
1456
+ }
1457
+ if (interaction.type === DISCORD_INTERACTION_APPLICATION_COMMAND && interaction.data) {
1458
+ if (interaction.data.name === "reply") {
1459
+ const threadId = interaction.channel_id;
1460
+ const content = interaction.data.options?.find(
1461
+ (opt) => opt.name === "message"
1462
+ )?.value;
1463
+ if (threadId && content) {
1464
+ const operatorName = interaction.member?.user?.username ?? interaction.user?.username ?? "Operator";
1465
+ await this.config.onOperatorMessage(
1466
+ threadId,
1467
+ content,
1468
+ operatorName,
1469
+ "discord",
1470
+ [],
1471
+ null
1472
+ );
1473
+ res.setHeader("Content-Type", "application/json");
1474
+ res.end(
1475
+ JSON.stringify({
1476
+ type: DISCORD_RESPONSE_CHANNEL_MESSAGE,
1477
+ data: { content: "\u2705 Message sent to visitor" }
1478
+ })
1479
+ );
1480
+ return;
1481
+ }
1482
+ }
1483
+ }
1484
+ res.setHeader("Content-Type", "application/json");
1485
+ res.end(JSON.stringify({ type: DISCORD_RESPONSE_PONG }));
1486
+ } catch (error) {
1487
+ console.error("[WebhookHandler] Discord error:", error);
1488
+ res.statusCode = 500;
1489
+ res.end(JSON.stringify({ error: "Internal server error" }));
1490
+ }
1491
+ };
1492
+ }
1493
+ // ─────────────────────────────────────────────────────────────────
1494
+ // Helper Methods
1495
+ // ─────────────────────────────────────────────────────────────────
1496
+ async parseBody(req) {
1497
+ if (req.body) return req.body;
1498
+ return new Promise((resolve, reject) => {
1499
+ let data = "";
1500
+ req.on("data", (chunk) => data += chunk);
1501
+ req.on("end", () => {
1502
+ try {
1503
+ resolve(data ? JSON.parse(data) : {});
1504
+ } catch {
1505
+ reject(new Error("Invalid JSON"));
1506
+ }
1507
+ });
1508
+ req.on("error", reject);
1509
+ });
1510
+ }
1511
+ writeOK(res) {
1512
+ res.setHeader("Content-Type", "application/json");
1513
+ res.end(JSON.stringify({ ok: true }));
1514
+ }
1515
+ async downloadTelegramFile(fileId) {
1516
+ try {
1517
+ const botToken = this.config.telegramBotToken;
1518
+ const getFileUrl = `https://api.telegram.org/bot${botToken}/getFile?file_id=${fileId}`;
1519
+ const getFileResp = await fetch(getFileUrl);
1520
+ const getFileResult = await getFileResp.json();
1521
+ if (!getFileResult.ok || !getFileResult.result?.file_path) {
1522
+ return null;
1523
+ }
1524
+ const downloadUrl = `https://api.telegram.org/file/bot${botToken}/${getFileResult.result.file_path}`;
1525
+ const downloadResp = await fetch(downloadUrl);
1526
+ const arrayBuffer = await downloadResp.arrayBuffer();
1527
+ return Buffer.from(arrayBuffer);
1528
+ } catch (error) {
1529
+ console.error("[WebhookHandler] Telegram file download error:", error);
1530
+ return null;
1531
+ }
1532
+ }
1533
+ async downloadSlackFile(file) {
1534
+ try {
1535
+ const downloadUrl = file.url_private_download ?? file.url_private;
1536
+ const resp = await fetch(downloadUrl, {
1537
+ headers: {
1538
+ Authorization: `Bearer ${this.config.slackBotToken}`
1539
+ }
1540
+ });
1541
+ if (!resp.ok) {
1542
+ return null;
1543
+ }
1544
+ const arrayBuffer = await resp.arrayBuffer();
1545
+ return Buffer.from(arrayBuffer);
1546
+ } catch (error) {
1547
+ console.error("[WebhookHandler] Slack file download error:", error);
1548
+ return null;
1549
+ }
1550
+ }
1551
+ async getSlackUserName(userId) {
1552
+ try {
1553
+ const url = `https://slack.com/api/users.info?user=${userId}`;
1554
+ const resp = await fetch(url, {
1555
+ headers: {
1556
+ Authorization: `Bearer ${this.config.slackBotToken}`
1557
+ }
1558
+ });
1559
+ const result = await resp.json();
1560
+ if (!result.ok) {
1561
+ return null;
1562
+ }
1563
+ return result.user?.real_name ?? result.user?.name ?? null;
1564
+ } catch {
1565
+ return null;
1566
+ }
1567
+ }
1568
+ };
1569
+
1570
+ // src/bridges/telegram.ts
1571
+ var TelegramBridge = class {
1572
+ constructor(botToken, chatId, options = {}) {
1573
+ this.name = "telegram";
1574
+ this.botToken = botToken;
1575
+ this.chatId = chatId;
1576
+ this.parseMode = options.parseMode ?? "HTML";
1577
+ this.disableNotification = options.disableNotification ?? false;
1578
+ this.baseUrl = `https://api.telegram.org/bot${botToken}`;
1579
+ }
1580
+ /**
1581
+ * Initialize the bridge (optional setup)
1582
+ */
1583
+ async init(pocketping) {
1584
+ this.pocketping = pocketping;
1585
+ try {
1586
+ const response = await fetch(`${this.baseUrl}/getMe`);
1587
+ const data = await response.json();
1588
+ if (!data.ok) {
1589
+ console.error("[TelegramBridge] Invalid bot token:", data.description);
1590
+ }
1591
+ } catch (error) {
1592
+ console.error("[TelegramBridge] Failed to verify bot token:", error);
1593
+ }
1594
+ }
1595
+ /**
1596
+ * Called when a new chat session is created
1597
+ */
1598
+ async onNewSession(session) {
1599
+ const url = session.metadata?.url || "Unknown page";
1600
+ const text = this.formatNewSession(session.visitorId, url);
1601
+ try {
1602
+ await this.sendMessage(text);
1603
+ } catch (error) {
1604
+ console.error("[TelegramBridge] Failed to send new session notification:", error);
1605
+ }
1606
+ }
1607
+ /**
1608
+ * Called when a visitor sends a message.
1609
+ * Returns the Telegram message ID for edit/delete sync.
1610
+ */
1611
+ async onVisitorMessage(message, session) {
1612
+ const text = this.formatVisitorMessage(session.visitorId, message.content);
1613
+ let replyToMessageId;
1614
+ if (message.replyTo) {
1615
+ const storage = this.pocketping?.getStorage();
1616
+ if (storage?.getBridgeMessageIds) {
1617
+ const ids = await storage.getBridgeMessageIds(message.replyTo);
1618
+ if (ids?.telegramMessageId) {
1619
+ replyToMessageId = ids.telegramMessageId;
1620
+ }
1621
+ }
1622
+ }
1623
+ try {
1624
+ const messageId = await this.sendMessage(text, replyToMessageId);
1625
+ return { messageId };
1626
+ } catch (error) {
1627
+ console.error("[TelegramBridge] Failed to send visitor message:", error);
1628
+ return {};
1629
+ }
1630
+ }
1631
+ /**
1632
+ * Called when an operator sends a message (for cross-bridge sync)
1633
+ */
1634
+ async onOperatorMessage(message, _session, sourceBridge, operatorName) {
1635
+ if (sourceBridge === "telegram") {
1636
+ return;
1637
+ }
1638
+ const name = operatorName || "Operator";
1639
+ const text = this.parseMode === "HTML" ? `<b>${this.escapeHtml(name)}:</b>
1640
+ ${this.escapeHtml(message.content)}` : `*${this.escapeMarkdown(name)}:*
1641
+ ${this.escapeMarkdown(message.content)}`;
1642
+ try {
1643
+ await this.sendMessage(text);
1644
+ } catch (error) {
1645
+ console.error("[TelegramBridge] Failed to send operator message:", error);
1646
+ }
1647
+ }
1648
+ /**
1649
+ * Called when visitor starts/stops typing
1650
+ */
1651
+ async onTyping(sessionId, isTyping) {
1652
+ if (!isTyping) return;
1653
+ try {
1654
+ await this.sendChatAction("typing");
1655
+ } catch (error) {
1656
+ console.error("[TelegramBridge] Failed to send typing action:", error);
1657
+ }
1658
+ }
1659
+ /**
1660
+ * Called when a visitor edits their message.
1661
+ * @returns true if edit succeeded, false otherwise
1662
+ */
1663
+ async onMessageEdit(_messageId, newContent, bridgeMessageId) {
1664
+ try {
1665
+ const response = await fetch(`${this.baseUrl}/editMessageText`, {
1666
+ method: "POST",
1667
+ headers: { "Content-Type": "application/json" },
1668
+ body: JSON.stringify({
1669
+ chat_id: this.chatId,
1670
+ message_id: bridgeMessageId,
1671
+ text: `${newContent}
1672
+
1673
+ <i>(edited)</i>`,
1674
+ parse_mode: this.parseMode
1675
+ })
1676
+ });
1677
+ const data = await response.json();
1678
+ if (!data.ok) {
1679
+ console.error("[TelegramBridge] Edit failed:", data.description);
1680
+ return false;
1681
+ }
1682
+ return true;
1683
+ } catch (error) {
1684
+ console.error("[TelegramBridge] Failed to edit message:", error);
1685
+ return false;
1686
+ }
1687
+ }
1688
+ /**
1689
+ * Called when a visitor deletes their message.
1690
+ * @returns true if delete succeeded, false otherwise
1691
+ */
1692
+ async onMessageDelete(_messageId, bridgeMessageId) {
1693
+ try {
1694
+ const response = await fetch(`${this.baseUrl}/deleteMessage`, {
1695
+ method: "POST",
1696
+ headers: { "Content-Type": "application/json" },
1697
+ body: JSON.stringify({
1698
+ chat_id: this.chatId,
1699
+ message_id: bridgeMessageId
1700
+ })
1701
+ });
1702
+ const data = await response.json();
1703
+ if (!data.ok) {
1704
+ console.error("[TelegramBridge] Delete failed:", data.description);
1705
+ return false;
1706
+ }
1707
+ return true;
1708
+ } catch (error) {
1709
+ console.error("[TelegramBridge] Failed to delete message:", error);
1710
+ return false;
1711
+ }
1712
+ }
1713
+ /**
1714
+ * Called when a custom event is triggered from the widget
1715
+ */
1716
+ async onCustomEvent(event, session) {
1717
+ const dataStr = event.data ? JSON.stringify(event.data, null, 2) : "";
1718
+ const text = this.parseMode === "HTML" ? `<b>Custom Event:</b> ${this.escapeHtml(event.name)}
1719
+ <b>Visitor:</b> ${this.escapeHtml(session.visitorId)}${dataStr ? `
1720
+ <pre>${this.escapeHtml(dataStr)}</pre>` : ""}` : `*Custom Event:* ${this.escapeMarkdown(event.name)}
1721
+ *Visitor:* ${this.escapeMarkdown(session.visitorId)}${dataStr ? `
1722
+ \`\`\`
1723
+ ${dataStr}
1724
+ \`\`\`` : ""}`;
1725
+ try {
1726
+ await this.sendMessage(text);
1727
+ } catch (error) {
1728
+ console.error("[TelegramBridge] Failed to send custom event:", error);
1729
+ }
1730
+ }
1731
+ /**
1732
+ * Called when a user identifies themselves via PocketPing.identify()
1733
+ */
1734
+ async onIdentityUpdate(session) {
1735
+ if (!session.identity) return;
1736
+ const identity = session.identity;
1737
+ let text;
1738
+ if (this.parseMode === "HTML") {
1739
+ text = `<b>User Identified</b>
1740
+ <b>ID:</b> ${this.escapeHtml(identity.id)}
1741
+ ` + (identity.name ? `<b>Name:</b> ${this.escapeHtml(identity.name)}
1742
+ ` : "") + (identity.email ? `<b>Email:</b> ${this.escapeHtml(identity.email)}` : "");
1743
+ } else {
1744
+ text = `*User Identified*
1745
+ *ID:* ${this.escapeMarkdown(identity.id)}
1746
+ ` + (identity.name ? `*Name:* ${this.escapeMarkdown(identity.name)}
1747
+ ` : "") + (identity.email ? `*Email:* ${this.escapeMarkdown(identity.email)}` : "");
1748
+ }
1749
+ try {
1750
+ await this.sendMessage(text.trim());
1751
+ } catch (error) {
1752
+ console.error("[TelegramBridge] Failed to send identity update:", error);
1753
+ }
1754
+ }
1755
+ // ─────────────────────────────────────────────────────────────────
1756
+ // Private helper methods
1757
+ // ─────────────────────────────────────────────────────────────────
1758
+ /**
1759
+ * Send a message to the Telegram chat
1760
+ */
1761
+ async sendMessage(text, replyToMessageId) {
1762
+ const response = await fetch(`${this.baseUrl}/sendMessage`, {
1763
+ method: "POST",
1764
+ headers: { "Content-Type": "application/json" },
1765
+ body: JSON.stringify({
1766
+ chat_id: this.chatId,
1767
+ text,
1768
+ parse_mode: this.parseMode,
1769
+ disable_notification: this.disableNotification,
1770
+ ...replyToMessageId ? { reply_to_message_id: replyToMessageId } : {}
1771
+ })
1772
+ });
1773
+ const data = await response.json();
1774
+ if (!data.ok) {
1775
+ throw new Error(`Telegram API error: ${data.description}`);
1776
+ }
1777
+ return data.result?.message_id;
1778
+ }
1779
+ /**
1780
+ * Send a chat action (e.g., "typing")
1781
+ */
1782
+ async sendChatAction(action) {
1783
+ await fetch(`${this.baseUrl}/sendChatAction`, {
1784
+ method: "POST",
1785
+ headers: { "Content-Type": "application/json" },
1786
+ body: JSON.stringify({
1787
+ chat_id: this.chatId,
1788
+ action
1789
+ })
1790
+ });
1791
+ }
1792
+ /**
1793
+ * Format new session notification
1794
+ */
1795
+ formatNewSession(visitorId, url) {
1796
+ if (this.parseMode === "HTML") {
1797
+ return `<b>New chat session</b>
1798
+ <b>Visitor:</b> ${this.escapeHtml(visitorId)}
1799
+ <b>Page:</b> ${this.escapeHtml(url)}`;
1800
+ }
1801
+ return `*New chat session*
1802
+ *Visitor:* ${this.escapeMarkdown(visitorId)}
1803
+ *Page:* ${this.escapeMarkdown(url)}`;
1804
+ }
1805
+ /**
1806
+ * Format visitor message
1807
+ */
1808
+ formatVisitorMessage(visitorId, content) {
1809
+ if (this.parseMode === "HTML") {
1810
+ return `<b>${this.escapeHtml(visitorId)}:</b>
1811
+ ${this.escapeHtml(content)}`;
1812
+ }
1813
+ return `*${this.escapeMarkdown(visitorId)}:*
1814
+ ${this.escapeMarkdown(content)}`;
1815
+ }
1816
+ /**
1817
+ * Escape HTML special characters
1818
+ */
1819
+ escapeHtml(text) {
1820
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1821
+ }
1822
+ /**
1823
+ * Escape Markdown special characters
1824
+ */
1825
+ escapeMarkdown(text) {
1826
+ return text.replace(/[_*[\]()~`>#+=|{}.!-]/g, "\\$&");
1827
+ }
1828
+ };
1829
+
1830
+ // src/bridges/discord.ts
1831
+ var DiscordBridge = class _DiscordBridge {
1832
+ constructor(config) {
1833
+ this.name = "discord";
1834
+ this.mode = config.mode;
1835
+ this.webhookUrl = config.webhookUrl;
1836
+ this.botToken = config.botToken;
1837
+ this.channelId = config.channelId;
1838
+ this.username = config.username;
1839
+ this.avatarUrl = config.avatarUrl;
1840
+ }
1841
+ /**
1842
+ * Create a Discord bridge using a webhook URL
1843
+ */
1844
+ static webhook(webhookUrl, options = {}) {
1845
+ return new _DiscordBridge({
1846
+ mode: "webhook",
1847
+ webhookUrl,
1848
+ username: options.username,
1849
+ avatarUrl: options.avatarUrl
1850
+ });
1851
+ }
1852
+ /**
1853
+ * Create a Discord bridge using a bot token
1854
+ */
1855
+ static bot(botToken, channelId, options = {}) {
1856
+ return new _DiscordBridge({
1857
+ mode: "bot",
1858
+ botToken,
1859
+ channelId,
1860
+ username: options.username,
1861
+ avatarUrl: options.avatarUrl
1862
+ });
1863
+ }
1864
+ /**
1865
+ * Initialize the bridge (optional setup)
1866
+ */
1867
+ async init(pocketping) {
1868
+ this.pocketping = pocketping;
1869
+ if (this.mode === "bot" && this.botToken) {
1870
+ try {
1871
+ const response = await fetch("https://discord.com/api/v10/users/@me", {
1872
+ headers: { Authorization: `Bot ${this.botToken}` }
1873
+ });
1874
+ if (!response.ok) {
1875
+ console.error("[DiscordBridge] Invalid bot token");
1876
+ }
1877
+ } catch (error) {
1878
+ console.error("[DiscordBridge] Failed to verify bot token:", error);
1879
+ }
1880
+ }
1881
+ }
1882
+ /**
1883
+ * Called when a new chat session is created
1884
+ */
1885
+ async onNewSession(session) {
1886
+ const url = session.metadata?.url || "Unknown page";
1887
+ const embed = {
1888
+ title: "New chat session",
1889
+ color: 5793266,
1890
+ // Discord blurple
1891
+ fields: [
1892
+ { name: "Visitor", value: session.visitorId, inline: true },
1893
+ { name: "Page", value: url, inline: false }
1894
+ ],
1895
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1896
+ };
1897
+ try {
1898
+ await this.sendEmbed(embed);
1899
+ } catch (error) {
1900
+ console.error("[DiscordBridge] Failed to send new session notification:", error);
1901
+ }
1902
+ }
1903
+ /**
1904
+ * Called when a visitor sends a message.
1905
+ * Returns the Discord message ID for edit/delete sync.
1906
+ */
1907
+ async onVisitorMessage(message, session) {
1908
+ const embed = {
1909
+ author: {
1910
+ name: session.visitorId,
1911
+ icon_url: this.avatarUrl
1912
+ },
1913
+ description: message.content,
1914
+ color: 5763719,
1915
+ // Green
1916
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1917
+ };
1918
+ let replyToMessageId;
1919
+ if (message.replyTo) {
1920
+ const storage = this.pocketping?.getStorage();
1921
+ if (storage?.getBridgeMessageIds) {
1922
+ const ids = await storage.getBridgeMessageIds(message.replyTo);
1923
+ if (ids?.discordMessageId) {
1924
+ replyToMessageId = ids.discordMessageId;
1925
+ }
1926
+ }
1927
+ }
1928
+ try {
1929
+ const messageId = await this.sendEmbed(embed, replyToMessageId);
1930
+ return { messageId };
1931
+ } catch (error) {
1932
+ console.error("[DiscordBridge] Failed to send visitor message:", error);
1933
+ return {};
1934
+ }
1935
+ }
1936
+ /**
1937
+ * Called when an operator sends a message (for cross-bridge sync)
1938
+ */
1939
+ async onOperatorMessage(message, _session, sourceBridge, operatorName) {
1940
+ if (sourceBridge === "discord") {
1941
+ return;
1942
+ }
1943
+ const embed = {
1944
+ author: {
1945
+ name: operatorName || "Operator",
1946
+ icon_url: this.avatarUrl
1947
+ },
1948
+ description: message.content,
1949
+ color: 16705372,
1950
+ // Yellow
1951
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1952
+ };
1953
+ try {
1954
+ await this.sendEmbed(embed);
1955
+ } catch (error) {
1956
+ console.error("[DiscordBridge] Failed to send operator message:", error);
1957
+ }
1958
+ }
1959
+ /**
1960
+ * Called when visitor starts/stops typing
1961
+ */
1962
+ async onTyping(_sessionId, isTyping) {
1963
+ if (!isTyping || this.mode !== "bot" || !this.channelId) return;
1964
+ try {
1965
+ await fetch(
1966
+ `https://discord.com/api/v10/channels/${this.channelId}/typing`,
1967
+ {
1968
+ method: "POST",
1969
+ headers: { Authorization: `Bot ${this.botToken}` }
1970
+ }
1971
+ );
1972
+ } catch (error) {
1973
+ console.error("[DiscordBridge] Failed to send typing indicator:", error);
1974
+ }
1975
+ }
1976
+ /**
1977
+ * Called when a visitor edits their message.
1978
+ * @returns true if edit succeeded, false otherwise
1979
+ */
1980
+ async onMessageEdit(_messageId, newContent, bridgeMessageId) {
1981
+ try {
1982
+ if (this.mode === "webhook" && this.webhookUrl) {
1983
+ const response = await fetch(
1984
+ `${this.webhookUrl}/messages/${bridgeMessageId}`,
1985
+ {
1986
+ method: "PATCH",
1987
+ headers: { "Content-Type": "application/json" },
1988
+ body: JSON.stringify({
1989
+ embeds: [
1990
+ {
1991
+ description: `${newContent}
1992
+
1993
+ *(edited)*`,
1994
+ color: 5763719
1995
+ }
1996
+ ]
1997
+ })
1998
+ }
1999
+ );
2000
+ return response.ok;
2001
+ } else if (this.mode === "bot" && this.channelId) {
2002
+ const response = await fetch(
2003
+ `https://discord.com/api/v10/channels/${this.channelId}/messages/${bridgeMessageId}`,
2004
+ {
2005
+ method: "PATCH",
2006
+ headers: {
2007
+ "Content-Type": "application/json",
2008
+ Authorization: `Bot ${this.botToken}`
2009
+ },
2010
+ body: JSON.stringify({
2011
+ embeds: [
2012
+ {
2013
+ description: `${newContent}
2014
+
2015
+ *(edited)*`,
2016
+ color: 5763719
2017
+ }
2018
+ ]
2019
+ })
2020
+ }
2021
+ );
2022
+ return response.ok;
2023
+ }
2024
+ return false;
2025
+ } catch (error) {
2026
+ console.error("[DiscordBridge] Failed to edit message:", error);
2027
+ return false;
2028
+ }
2029
+ }
2030
+ /**
2031
+ * Called when a visitor deletes their message.
2032
+ * @returns true if delete succeeded, false otherwise
2033
+ */
2034
+ async onMessageDelete(_messageId, bridgeMessageId) {
2035
+ try {
2036
+ if (this.mode === "webhook" && this.webhookUrl) {
2037
+ const response = await fetch(
2038
+ `${this.webhookUrl}/messages/${bridgeMessageId}`,
2039
+ { method: "DELETE" }
2040
+ );
2041
+ return response.ok || response.status === 404;
2042
+ } else if (this.mode === "bot" && this.channelId) {
2043
+ const response = await fetch(
2044
+ `https://discord.com/api/v10/channels/${this.channelId}/messages/${bridgeMessageId}`,
2045
+ {
2046
+ method: "DELETE",
2047
+ headers: { Authorization: `Bot ${this.botToken}` }
2048
+ }
2049
+ );
2050
+ return response.ok || response.status === 404;
2051
+ }
2052
+ return false;
2053
+ } catch (error) {
2054
+ console.error("[DiscordBridge] Failed to delete message:", error);
2055
+ return false;
2056
+ }
2057
+ }
2058
+ /**
2059
+ * Called when a custom event is triggered from the widget
2060
+ */
2061
+ async onCustomEvent(event, session) {
2062
+ const embed = {
2063
+ title: `Custom Event: ${event.name}`,
2064
+ color: 15418782,
2065
+ // Fuchsia
2066
+ fields: [
2067
+ { name: "Visitor", value: session.visitorId, inline: true },
2068
+ ...event.data ? [
2069
+ {
2070
+ name: "Data",
2071
+ value: `\`\`\`json
2072
+ ${JSON.stringify(event.data, null, 2)}
2073
+ \`\`\``,
2074
+ inline: false
2075
+ }
2076
+ ] : []
2077
+ ],
2078
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2079
+ };
2080
+ try {
2081
+ await this.sendEmbed(embed);
2082
+ } catch (error) {
2083
+ console.error("[DiscordBridge] Failed to send custom event:", error);
2084
+ }
2085
+ }
2086
+ /**
2087
+ * Called when a user identifies themselves via PocketPing.identify()
2088
+ */
2089
+ async onIdentityUpdate(session) {
2090
+ if (!session.identity) return;
2091
+ const identity = session.identity;
2092
+ const fields = [
2093
+ { name: "User ID", value: identity.id, inline: true }
2094
+ ];
2095
+ if (identity.name) {
2096
+ fields.push({ name: "Name", value: identity.name, inline: true });
2097
+ }
2098
+ if (identity.email) {
2099
+ fields.push({ name: "Email", value: identity.email, inline: true });
2100
+ }
2101
+ const embed = {
2102
+ title: "User Identified",
2103
+ color: 5793266,
2104
+ fields,
2105
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2106
+ };
2107
+ try {
2108
+ await this.sendEmbed(embed);
2109
+ } catch (error) {
2110
+ console.error("[DiscordBridge] Failed to send identity update:", error);
2111
+ }
2112
+ }
2113
+ // ─────────────────────────────────────────────────────────────────
2114
+ // Private helper methods
2115
+ // ─────────────────────────────────────────────────────────────────
2116
+ /**
2117
+ * Send an embed to Discord
2118
+ */
2119
+ async sendEmbed(embed, replyToMessageId) {
2120
+ const body = {
2121
+ embeds: [embed]
2122
+ };
2123
+ if (this.username) {
2124
+ body.username = this.username;
2125
+ }
2126
+ if (this.avatarUrl) {
2127
+ body.avatar_url = this.avatarUrl;
2128
+ }
2129
+ if (this.mode === "webhook" && this.webhookUrl) {
2130
+ if (replyToMessageId) {
2131
+ body.message_reference = { message_id: replyToMessageId };
2132
+ }
2133
+ const response = await fetch(`${this.webhookUrl}?wait=true`, {
2134
+ method: "POST",
2135
+ headers: { "Content-Type": "application/json" },
2136
+ body: JSON.stringify(body)
2137
+ });
2138
+ if (!response.ok) {
2139
+ const error = await response.text();
2140
+ throw new Error(`Discord webhook error: ${error}`);
2141
+ }
2142
+ const data = await response.json();
2143
+ return data.id;
2144
+ } else if (this.mode === "bot" && this.channelId) {
2145
+ if (replyToMessageId) {
2146
+ body.message_reference = { message_id: replyToMessageId };
2147
+ }
2148
+ const response = await fetch(
2149
+ `https://discord.com/api/v10/channels/${this.channelId}/messages`,
2150
+ {
2151
+ method: "POST",
2152
+ headers: {
2153
+ "Content-Type": "application/json",
2154
+ Authorization: `Bot ${this.botToken}`
2155
+ },
2156
+ body: JSON.stringify(body)
2157
+ }
2158
+ );
2159
+ if (!response.ok) {
2160
+ const error = await response.text();
2161
+ throw new Error(`Discord API error: ${error}`);
2162
+ }
2163
+ const data = await response.json();
2164
+ return data.id;
2165
+ }
2166
+ return void 0;
2167
+ }
2168
+ };
2169
+
2170
+ // src/bridges/slack.ts
2171
+ var SlackBridge = class _SlackBridge {
2172
+ constructor(config) {
2173
+ this.name = "slack";
2174
+ this.mode = config.mode;
2175
+ this.webhookUrl = config.webhookUrl;
2176
+ this.botToken = config.botToken;
2177
+ this.channelId = config.channelId;
2178
+ this.username = config.username;
2179
+ this.iconEmoji = config.iconEmoji;
2180
+ this.iconUrl = config.iconUrl;
2181
+ }
2182
+ /**
2183
+ * Create a Slack bridge using a webhook URL
2184
+ */
2185
+ static webhook(webhookUrl, options = {}) {
2186
+ return new _SlackBridge({
2187
+ mode: "webhook",
2188
+ webhookUrl,
2189
+ username: options.username,
2190
+ iconEmoji: options.iconEmoji,
2191
+ iconUrl: options.iconUrl
2192
+ });
2193
+ }
2194
+ /**
2195
+ * Create a Slack bridge using a bot token
2196
+ */
2197
+ static bot(botToken, channelId, options = {}) {
2198
+ return new _SlackBridge({
2199
+ mode: "bot",
2200
+ botToken,
2201
+ channelId,
2202
+ username: options.username,
2203
+ iconEmoji: options.iconEmoji,
2204
+ iconUrl: options.iconUrl
2205
+ });
2206
+ }
2207
+ /**
2208
+ * Initialize the bridge (optional setup)
2209
+ */
2210
+ async init(pocketping) {
2211
+ this.pocketping = pocketping;
2212
+ if (this.mode === "bot" && this.botToken) {
2213
+ try {
2214
+ const response = await fetch("https://slack.com/api/auth.test", {
2215
+ method: "POST",
2216
+ headers: {
2217
+ "Content-Type": "application/json",
2218
+ Authorization: `Bearer ${this.botToken}`
2219
+ }
2220
+ });
2221
+ const data = await response.json();
2222
+ if (!data.ok) {
2223
+ console.error("[SlackBridge] Invalid bot token:", data.error);
2224
+ }
2225
+ } catch (error) {
2226
+ console.error("[SlackBridge] Failed to verify bot token:", error);
2227
+ }
2228
+ }
2229
+ }
2230
+ /**
2231
+ * Called when a new chat session is created
2232
+ */
2233
+ async onNewSession(session) {
2234
+ const url = session.metadata?.url || "Unknown page";
2235
+ const blocks = [
2236
+ {
2237
+ type: "header",
2238
+ text: {
2239
+ type: "plain_text",
2240
+ text: "New chat session",
2241
+ emoji: true
2242
+ }
2243
+ },
2244
+ {
2245
+ type: "section",
2246
+ fields: [
2247
+ {
2248
+ type: "mrkdwn",
2249
+ text: `*Visitor:*
2250
+ ${session.visitorId}`
2251
+ },
2252
+ {
2253
+ type: "mrkdwn",
2254
+ text: `*Page:*
2255
+ ${url}`
2256
+ }
2257
+ ]
2258
+ }
2259
+ ];
2260
+ try {
2261
+ await this.sendBlocks(blocks);
2262
+ } catch (error) {
2263
+ console.error("[SlackBridge] Failed to send new session notification:", error);
2264
+ }
2265
+ }
2266
+ /**
2267
+ * Called when a visitor sends a message.
2268
+ * Returns the Slack message timestamp for edit/delete sync.
2269
+ */
2270
+ async onVisitorMessage(message, session) {
2271
+ const blocks = [];
2272
+ if (message.replyTo && this.pocketping?.getStorage().getMessage) {
2273
+ const replyTarget = await this.pocketping.getStorage().getMessage(message.replyTo);
2274
+ if (replyTarget) {
2275
+ const senderLabel = replyTarget.sender === "visitor" ? "Visitor" : replyTarget.sender === "operator" ? "Support" : "AI";
2276
+ const rawPreview = replyTarget.deletedAt ? "Message deleted" : replyTarget.content || "Message";
2277
+ const preview = rawPreview.length > 140 ? `${rawPreview.slice(0, 140)}...` : rawPreview;
2278
+ const quoted = `> *${this.escapeSlack(senderLabel)}* \u2014 ${this.escapeSlack(preview)}`;
2279
+ blocks.push({
2280
+ type: "section",
2281
+ text: {
2282
+ type: "mrkdwn",
2283
+ text: quoted
2284
+ }
2285
+ });
2286
+ }
2287
+ }
2288
+ blocks.push(
2289
+ {
2290
+ type: "section",
2291
+ text: {
2292
+ type: "mrkdwn",
2293
+ text: `*${this.escapeSlack(session.visitorId)}:*
2294
+ ${this.escapeSlack(message.content)}`
2295
+ }
2296
+ },
2297
+ {
2298
+ type: "context",
2299
+ elements: [
2300
+ {
2301
+ type: "mrkdwn",
2302
+ text: `<!date^${Math.floor(Date.now() / 1e3)}^{date_short_pretty} at {time}|${(/* @__PURE__ */ new Date()).toISOString()}>`
2303
+ }
2304
+ ]
2305
+ }
2306
+ );
2307
+ try {
2308
+ const messageId = await this.sendBlocks(blocks);
2309
+ return { messageId };
2310
+ } catch (error) {
2311
+ console.error("[SlackBridge] Failed to send visitor message:", error);
2312
+ return {};
2313
+ }
2314
+ }
2315
+ /**
2316
+ * Called when an operator sends a message (for cross-bridge sync)
2317
+ */
2318
+ async onOperatorMessage(message, _session, sourceBridge, operatorName) {
2319
+ if (sourceBridge === "slack") {
2320
+ return;
2321
+ }
2322
+ const name = operatorName || "Operator";
2323
+ const blocks = [
2324
+ {
2325
+ type: "section",
2326
+ text: {
2327
+ type: "mrkdwn",
2328
+ text: `*${this.escapeSlack(name)}:*
2329
+ ${this.escapeSlack(message.content)}`
2330
+ }
2331
+ }
2332
+ ];
2333
+ try {
2334
+ await this.sendBlocks(blocks);
2335
+ } catch (error) {
2336
+ console.error("[SlackBridge] Failed to send operator message:", error);
2337
+ }
2338
+ }
2339
+ /**
2340
+ * Called when visitor starts/stops typing
2341
+ */
2342
+ async onTyping(_sessionId, _isTyping) {
2343
+ }
2344
+ /**
2345
+ * Called when a visitor edits their message.
2346
+ * @returns true if edit succeeded, false otherwise
2347
+ */
2348
+ async onMessageEdit(_messageId, newContent, bridgeMessageId) {
2349
+ if (this.mode !== "bot" || !this.channelId) {
2350
+ console.warn("[SlackBridge] Message edit only supported in bot mode");
2351
+ return false;
2352
+ }
2353
+ try {
2354
+ const response = await fetch("https://slack.com/api/chat.update", {
2355
+ method: "POST",
2356
+ headers: {
2357
+ "Content-Type": "application/json",
2358
+ Authorization: `Bearer ${this.botToken}`
2359
+ },
2360
+ body: JSON.stringify({
2361
+ channel: this.channelId,
2362
+ ts: bridgeMessageId,
2363
+ blocks: [
2364
+ {
2365
+ type: "section",
2366
+ text: {
2367
+ type: "mrkdwn",
2368
+ text: `${this.escapeSlack(newContent)}
2369
+
2370
+ _(edited)_`
2371
+ }
2372
+ }
2373
+ ]
2374
+ })
2375
+ });
2376
+ const data = await response.json();
2377
+ if (!data.ok) {
2378
+ console.error("[SlackBridge] Edit failed:", data.error);
2379
+ return false;
2380
+ }
2381
+ return true;
2382
+ } catch (error) {
2383
+ console.error("[SlackBridge] Failed to edit message:", error);
2384
+ return false;
2385
+ }
2386
+ }
2387
+ /**
2388
+ * Called when a visitor deletes their message.
2389
+ * @returns true if delete succeeded, false otherwise
2390
+ */
2391
+ async onMessageDelete(_messageId, bridgeMessageId) {
2392
+ if (this.mode !== "bot" || !this.channelId) {
2393
+ console.warn("[SlackBridge] Message delete only supported in bot mode");
2394
+ return false;
2395
+ }
2396
+ try {
2397
+ const response = await fetch("https://slack.com/api/chat.delete", {
2398
+ method: "POST",
2399
+ headers: {
2400
+ "Content-Type": "application/json",
2401
+ Authorization: `Bearer ${this.botToken}`
2402
+ },
2403
+ body: JSON.stringify({
2404
+ channel: this.channelId,
2405
+ ts: bridgeMessageId
2406
+ })
2407
+ });
2408
+ const data = await response.json();
2409
+ if (!data.ok) {
2410
+ if (data.error === "message_not_found") {
2411
+ return true;
2412
+ }
2413
+ console.error("[SlackBridge] Delete failed:", data.error);
2414
+ return false;
2415
+ }
2416
+ return true;
2417
+ } catch (error) {
2418
+ console.error("[SlackBridge] Failed to delete message:", error);
2419
+ return false;
2420
+ }
2421
+ }
2422
+ /**
2423
+ * Called when a custom event is triggered from the widget
2424
+ */
2425
+ async onCustomEvent(event, session) {
2426
+ const blocks = [
2427
+ {
2428
+ type: "header",
2429
+ text: {
2430
+ type: "plain_text",
2431
+ text: `Custom Event: ${event.name}`,
2432
+ emoji: true
2433
+ }
2434
+ },
2435
+ {
2436
+ type: "section",
2437
+ fields: [
2438
+ {
2439
+ type: "mrkdwn",
2440
+ text: `*Visitor:*
2441
+ ${session.visitorId}`
2442
+ }
2443
+ ]
2444
+ }
2445
+ ];
2446
+ if (event.data) {
2447
+ blocks.push({
2448
+ type: "section",
2449
+ text: {
2450
+ type: "mrkdwn",
2451
+ text: `*Data:*
2452
+ \`\`\`${JSON.stringify(event.data, null, 2)}\`\`\``
2453
+ }
2454
+ });
2455
+ }
2456
+ try {
2457
+ await this.sendBlocks(blocks);
2458
+ } catch (error) {
2459
+ console.error("[SlackBridge] Failed to send custom event:", error);
2460
+ }
2461
+ }
2462
+ /**
2463
+ * Called when a user identifies themselves via PocketPing.identify()
2464
+ */
2465
+ async onIdentityUpdate(session) {
2466
+ if (!session.identity) return;
2467
+ const identity = session.identity;
2468
+ const fields = [
2469
+ {
2470
+ type: "mrkdwn",
2471
+ text: `*User ID:*
2472
+ ${identity.id}`
2473
+ }
2474
+ ];
2475
+ if (identity.name) {
2476
+ fields.push({
2477
+ type: "mrkdwn",
2478
+ text: `*Name:*
2479
+ ${identity.name}`
2480
+ });
2481
+ }
2482
+ if (identity.email) {
2483
+ fields.push({
2484
+ type: "mrkdwn",
2485
+ text: `*Email:*
2486
+ ${identity.email}`
2487
+ });
2488
+ }
2489
+ const blocks = [
2490
+ {
2491
+ type: "header",
2492
+ text: {
2493
+ type: "plain_text",
2494
+ text: "User Identified",
2495
+ emoji: true
2496
+ }
2497
+ },
2498
+ {
2499
+ type: "section",
2500
+ fields
2501
+ }
2502
+ ];
2503
+ try {
2504
+ await this.sendBlocks(blocks);
2505
+ } catch (error) {
2506
+ console.error("[SlackBridge] Failed to send identity update:", error);
2507
+ }
2508
+ }
2509
+ // ─────────────────────────────────────────────────────────────────
2510
+ // Private helper methods
2511
+ // ─────────────────────────────────────────────────────────────────
2512
+ /**
2513
+ * Send blocks to Slack
2514
+ */
2515
+ async sendBlocks(blocks) {
2516
+ const payload = { blocks };
2517
+ if (this.username) {
2518
+ payload.username = this.username;
2519
+ }
2520
+ if (this.iconUrl) {
2521
+ payload.icon_url = this.iconUrl;
2522
+ } else if (this.iconEmoji) {
2523
+ payload.icon_emoji = this.iconEmoji;
2524
+ }
2525
+ if (this.mode === "webhook" && this.webhookUrl) {
2526
+ const response = await fetch(this.webhookUrl, {
2527
+ method: "POST",
2528
+ headers: { "Content-Type": "application/json" },
2529
+ body: JSON.stringify(payload)
2530
+ });
2531
+ if (!response.ok) {
2532
+ const error = await response.text();
2533
+ throw new Error(`Slack webhook error: ${error}`);
2534
+ }
2535
+ return void 0;
2536
+ } else if (this.mode === "bot" && this.channelId) {
2537
+ payload.channel = this.channelId;
2538
+ const response = await fetch("https://slack.com/api/chat.postMessage", {
2539
+ method: "POST",
2540
+ headers: {
2541
+ "Content-Type": "application/json",
2542
+ Authorization: `Bearer ${this.botToken}`
2543
+ },
2544
+ body: JSON.stringify(payload)
2545
+ });
2546
+ const data = await response.json();
2547
+ if (!data.ok) {
2548
+ throw new Error(`Slack API error: ${data.error}`);
2549
+ }
2550
+ return data.ts;
2551
+ }
2552
+ return void 0;
2553
+ }
2554
+ /**
2555
+ * Escape special characters for Slack mrkdwn
2556
+ */
2557
+ escapeSlack(text) {
2558
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2559
+ }
2560
+ };
997
2561
  // Annotate the CommonJS export names for ESM import in node:
998
2562
  0 && (module.exports = {
2563
+ DiscordBridge,
999
2564
  MemoryStorage,
1000
- PocketPing
2565
+ PocketPing,
2566
+ SlackBridge,
2567
+ TelegramBridge,
2568
+ WebhookHandler
1001
2569
  });