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