@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/README.md +137 -6
- package/dist/index.cjs +1572 -4
- package/dist/index.d.cts +491 -3
- package/dist/index.d.ts +491 -3
- package/dist/index.js +1567 -3
- package/package.json +1 -1
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
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
});
|