@pocketping/sdk-node 0.1.0 → 1.0.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 +305 -0
- package/dist/index.cjs +884 -0
- package/dist/index.d.cts +421 -0
- package/dist/index.d.ts +202 -3
- package/dist/index.js +408 -48
- package/package.json +33 -5
- package/dist/index.d.mts +0 -222
- package/dist/index.mjs +0 -468
package/dist/index.d.mts
DELETED
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
import { IncomingMessage, ServerResponse } from 'http';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Storage adapter interface.
|
|
5
|
-
* Implement this interface to use any database with PocketPing.
|
|
6
|
-
*/
|
|
7
|
-
interface Storage {
|
|
8
|
-
createSession(session: Session): Promise<void>;
|
|
9
|
-
getSession(sessionId: string): Promise<Session | null>;
|
|
10
|
-
getSessionByVisitorId?(visitorId: string): Promise<Session | null>;
|
|
11
|
-
updateSession(session: Session): Promise<void>;
|
|
12
|
-
deleteSession(sessionId: string): Promise<void>;
|
|
13
|
-
saveMessage(message: Message): Promise<void>;
|
|
14
|
-
getMessages(sessionId: string, after?: string, limit?: number): Promise<Message[]>;
|
|
15
|
-
getMessage(messageId: string): Promise<Message | null>;
|
|
16
|
-
cleanupOldSessions?(olderThan: Date): Promise<number>;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Bridge interface for notification channels.
|
|
21
|
-
* Implement this interface to add support for Telegram, Discord, Slack, etc.
|
|
22
|
-
*/
|
|
23
|
-
interface Bridge {
|
|
24
|
-
/** Unique name for this bridge */
|
|
25
|
-
name: string;
|
|
26
|
-
/** Called when the bridge is added to PocketPing */
|
|
27
|
-
init?(pocketping: PocketPing): void | Promise<void>;
|
|
28
|
-
/** Called when a new chat session is created */
|
|
29
|
-
onNewSession?(session: Session): void | Promise<void>;
|
|
30
|
-
/** Called when a visitor sends a message */
|
|
31
|
-
onMessage?(message: Message, session: Session): void | Promise<void>;
|
|
32
|
-
/** Called when visitor starts/stops typing */
|
|
33
|
-
onTyping?(sessionId: string, isTyping: boolean): void | Promise<void>;
|
|
34
|
-
/** Called when messages are marked as delivered/read */
|
|
35
|
-
onMessageRead?(sessionId: string, messageIds: string[], status: MessageStatus): void | Promise<void>;
|
|
36
|
-
/** Cleanup when bridge is removed */
|
|
37
|
-
destroy?(): void | Promise<void>;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* AI provider interface.
|
|
42
|
-
* Implement this to add support for OpenAI, Gemini, Claude, or local models.
|
|
43
|
-
*/
|
|
44
|
-
interface AIProvider {
|
|
45
|
-
/** Provider name */
|
|
46
|
-
name: string;
|
|
47
|
-
/** Generate a response to the conversation */
|
|
48
|
-
generateResponse(messages: Message[], systemPrompt?: string): Promise<string>;
|
|
49
|
-
/** Check if the provider is available */
|
|
50
|
-
isAvailable(): Promise<boolean>;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
interface PocketPingConfig {
|
|
54
|
-
/** Storage adapter for sessions and messages */
|
|
55
|
-
storage?: Storage | 'memory';
|
|
56
|
-
/** Notification bridges (Telegram, Discord, etc.) */
|
|
57
|
-
bridges?: Bridge[];
|
|
58
|
-
/** AI fallback configuration */
|
|
59
|
-
ai?: AIConfig;
|
|
60
|
-
/** Welcome message shown to new visitors */
|
|
61
|
-
welcomeMessage?: string;
|
|
62
|
-
/** Seconds of inactivity before AI takes over (default: 300) */
|
|
63
|
-
aiTakeoverDelay?: number;
|
|
64
|
-
/** Callback when a new session is created */
|
|
65
|
-
onNewSession?: (session: Session) => void | Promise<void>;
|
|
66
|
-
/** Callback when a message is received */
|
|
67
|
-
onMessage?: (message: Message, session: Session) => void | Promise<void>;
|
|
68
|
-
}
|
|
69
|
-
interface AIConfig {
|
|
70
|
-
provider: AIProvider | 'openai' | 'gemini' | 'anthropic';
|
|
71
|
-
apiKey?: string;
|
|
72
|
-
model?: string;
|
|
73
|
-
systemPrompt?: string;
|
|
74
|
-
fallbackAfter?: number;
|
|
75
|
-
}
|
|
76
|
-
interface Session {
|
|
77
|
-
id: string;
|
|
78
|
-
visitorId: string;
|
|
79
|
-
createdAt: Date;
|
|
80
|
-
lastActivity: Date;
|
|
81
|
-
operatorOnline: boolean;
|
|
82
|
-
aiActive: boolean;
|
|
83
|
-
metadata?: SessionMetadata;
|
|
84
|
-
}
|
|
85
|
-
interface SessionMetadata {
|
|
86
|
-
url?: string;
|
|
87
|
-
referrer?: string;
|
|
88
|
-
pageTitle?: string;
|
|
89
|
-
userAgent?: string;
|
|
90
|
-
timezone?: string;
|
|
91
|
-
language?: string;
|
|
92
|
-
screenResolution?: string;
|
|
93
|
-
ip?: string;
|
|
94
|
-
country?: string;
|
|
95
|
-
city?: string;
|
|
96
|
-
deviceType?: 'desktop' | 'mobile' | 'tablet';
|
|
97
|
-
browser?: string;
|
|
98
|
-
os?: string;
|
|
99
|
-
[key: string]: unknown;
|
|
100
|
-
}
|
|
101
|
-
type MessageStatus = 'sending' | 'sent' | 'delivered' | 'read';
|
|
102
|
-
interface Message {
|
|
103
|
-
id: string;
|
|
104
|
-
sessionId: string;
|
|
105
|
-
content: string;
|
|
106
|
-
sender: 'visitor' | 'operator' | 'ai';
|
|
107
|
-
timestamp: Date;
|
|
108
|
-
replyTo?: string;
|
|
109
|
-
metadata?: Record<string, unknown>;
|
|
110
|
-
status?: MessageStatus;
|
|
111
|
-
deliveredAt?: Date;
|
|
112
|
-
readAt?: Date;
|
|
113
|
-
}
|
|
114
|
-
interface ConnectRequest {
|
|
115
|
-
visitorId: string;
|
|
116
|
-
sessionId?: string;
|
|
117
|
-
metadata?: SessionMetadata;
|
|
118
|
-
}
|
|
119
|
-
interface ConnectResponse {
|
|
120
|
-
sessionId: string;
|
|
121
|
-
visitorId: string;
|
|
122
|
-
operatorOnline: boolean;
|
|
123
|
-
welcomeMessage?: string;
|
|
124
|
-
messages: Message[];
|
|
125
|
-
}
|
|
126
|
-
interface SendMessageRequest {
|
|
127
|
-
sessionId: string;
|
|
128
|
-
content: string;
|
|
129
|
-
sender: 'visitor' | 'operator';
|
|
130
|
-
replyTo?: string;
|
|
131
|
-
}
|
|
132
|
-
interface SendMessageResponse {
|
|
133
|
-
messageId: string;
|
|
134
|
-
timestamp: string;
|
|
135
|
-
}
|
|
136
|
-
interface GetMessagesRequest {
|
|
137
|
-
sessionId: string;
|
|
138
|
-
after?: string;
|
|
139
|
-
limit?: number;
|
|
140
|
-
}
|
|
141
|
-
interface GetMessagesResponse {
|
|
142
|
-
messages: Message[];
|
|
143
|
-
hasMore: boolean;
|
|
144
|
-
}
|
|
145
|
-
interface TypingRequest {
|
|
146
|
-
sessionId: string;
|
|
147
|
-
sender: 'visitor' | 'operator';
|
|
148
|
-
isTyping?: boolean;
|
|
149
|
-
}
|
|
150
|
-
interface ReadRequest {
|
|
151
|
-
sessionId: string;
|
|
152
|
-
messageIds: string[];
|
|
153
|
-
status?: MessageStatus;
|
|
154
|
-
}
|
|
155
|
-
interface ReadResponse {
|
|
156
|
-
updated: number;
|
|
157
|
-
}
|
|
158
|
-
interface PresenceResponse {
|
|
159
|
-
online: boolean;
|
|
160
|
-
operators?: Array<{
|
|
161
|
-
id: string;
|
|
162
|
-
name: string;
|
|
163
|
-
avatar?: string;
|
|
164
|
-
}>;
|
|
165
|
-
aiEnabled: boolean;
|
|
166
|
-
aiActiveAfter?: number;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
declare class PocketPing {
|
|
170
|
-
private storage;
|
|
171
|
-
private bridges;
|
|
172
|
-
private config;
|
|
173
|
-
private wss;
|
|
174
|
-
private sessionSockets;
|
|
175
|
-
private operatorOnline;
|
|
176
|
-
constructor(config?: PocketPingConfig);
|
|
177
|
-
private initStorage;
|
|
178
|
-
middleware(): (req: IncomingMessage & {
|
|
179
|
-
body?: unknown;
|
|
180
|
-
query?: Record<string, string>;
|
|
181
|
-
}, res: ServerResponse, next?: () => void) => void;
|
|
182
|
-
private parseBody;
|
|
183
|
-
attachWebSocket(server: any): void;
|
|
184
|
-
private handleWebSocketMessage;
|
|
185
|
-
private broadcastToSession;
|
|
186
|
-
handleConnect(request: ConnectRequest): Promise<ConnectResponse>;
|
|
187
|
-
handleMessage(request: SendMessageRequest): Promise<SendMessageResponse>;
|
|
188
|
-
handleGetMessages(request: GetMessagesRequest): Promise<GetMessagesResponse>;
|
|
189
|
-
handleTyping(request: TypingRequest): Promise<{
|
|
190
|
-
ok: boolean;
|
|
191
|
-
}>;
|
|
192
|
-
handlePresence(): Promise<PresenceResponse>;
|
|
193
|
-
handleRead(request: ReadRequest): Promise<ReadResponse>;
|
|
194
|
-
sendOperatorMessage(sessionId: string, content: string): Promise<Message>;
|
|
195
|
-
setOperatorOnline(online: boolean): void;
|
|
196
|
-
private notifyBridges;
|
|
197
|
-
private notifyBridgesRead;
|
|
198
|
-
addBridge(bridge: Bridge): void;
|
|
199
|
-
private generateId;
|
|
200
|
-
getStorage(): Storage;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* In-memory storage adapter.
|
|
205
|
-
* Useful for development and testing. Data is lost on restart.
|
|
206
|
-
*/
|
|
207
|
-
declare class MemoryStorage implements Storage {
|
|
208
|
-
private sessions;
|
|
209
|
-
private messages;
|
|
210
|
-
private messageById;
|
|
211
|
-
createSession(session: Session): Promise<void>;
|
|
212
|
-
getSession(sessionId: string): Promise<Session | null>;
|
|
213
|
-
getSessionByVisitorId(visitorId: string): Promise<Session | null>;
|
|
214
|
-
updateSession(session: Session): Promise<void>;
|
|
215
|
-
deleteSession(sessionId: string): Promise<void>;
|
|
216
|
-
saveMessage(message: Message): Promise<void>;
|
|
217
|
-
getMessages(sessionId: string, after?: string, limit?: number): Promise<Message[]>;
|
|
218
|
-
getMessage(messageId: string): Promise<Message | null>;
|
|
219
|
-
cleanupOldSessions(olderThan: Date): Promise<number>;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export { type AIProvider, type Bridge, type ConnectRequest, type ConnectResponse, MemoryStorage, type Message, PocketPing, type PocketPingConfig, type PresenceResponse, type SendMessageRequest, type SendMessageResponse, type Session, type Storage };
|
package/dist/index.mjs
DELETED
|
@@ -1,468 +0,0 @@
|
|
|
1
|
-
// src/pocketping.ts
|
|
2
|
-
import { WebSocketServer, WebSocket } from "ws";
|
|
3
|
-
|
|
4
|
-
// src/storage/memory.ts
|
|
5
|
-
var MemoryStorage = class {
|
|
6
|
-
constructor() {
|
|
7
|
-
this.sessions = /* @__PURE__ */ new Map();
|
|
8
|
-
this.messages = /* @__PURE__ */ new Map();
|
|
9
|
-
this.messageById = /* @__PURE__ */ new Map();
|
|
10
|
-
}
|
|
11
|
-
async createSession(session) {
|
|
12
|
-
this.sessions.set(session.id, session);
|
|
13
|
-
this.messages.set(session.id, []);
|
|
14
|
-
}
|
|
15
|
-
async getSession(sessionId) {
|
|
16
|
-
return this.sessions.get(sessionId) ?? null;
|
|
17
|
-
}
|
|
18
|
-
async getSessionByVisitorId(visitorId) {
|
|
19
|
-
const visitorSessions = Array.from(this.sessions.values()).filter(
|
|
20
|
-
(s) => s.visitorId === visitorId
|
|
21
|
-
);
|
|
22
|
-
if (visitorSessions.length === 0) return null;
|
|
23
|
-
return visitorSessions.reduce(
|
|
24
|
-
(latest, s) => s.lastActivity > latest.lastActivity ? s : latest
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
async updateSession(session) {
|
|
28
|
-
this.sessions.set(session.id, session);
|
|
29
|
-
}
|
|
30
|
-
async deleteSession(sessionId) {
|
|
31
|
-
this.sessions.delete(sessionId);
|
|
32
|
-
this.messages.delete(sessionId);
|
|
33
|
-
}
|
|
34
|
-
async saveMessage(message) {
|
|
35
|
-
const sessionMessages = this.messages.get(message.sessionId) ?? [];
|
|
36
|
-
sessionMessages.push(message);
|
|
37
|
-
this.messages.set(message.sessionId, sessionMessages);
|
|
38
|
-
this.messageById.set(message.id, message);
|
|
39
|
-
}
|
|
40
|
-
async getMessages(sessionId, after, limit = 50) {
|
|
41
|
-
const sessionMessages = this.messages.get(sessionId) ?? [];
|
|
42
|
-
let startIndex = 0;
|
|
43
|
-
if (after) {
|
|
44
|
-
const afterIndex = sessionMessages.findIndex((m) => m.id === after);
|
|
45
|
-
if (afterIndex !== -1) {
|
|
46
|
-
startIndex = afterIndex + 1;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return sessionMessages.slice(startIndex, startIndex + limit);
|
|
50
|
-
}
|
|
51
|
-
async getMessage(messageId) {
|
|
52
|
-
return this.messageById.get(messageId) ?? null;
|
|
53
|
-
}
|
|
54
|
-
async cleanupOldSessions(olderThan) {
|
|
55
|
-
let count = 0;
|
|
56
|
-
for (const [id, session] of this.sessions) {
|
|
57
|
-
if (session.lastActivity < olderThan) {
|
|
58
|
-
this.sessions.delete(id);
|
|
59
|
-
this.messages.delete(id);
|
|
60
|
-
count++;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return count;
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
// src/pocketping.ts
|
|
68
|
-
function getClientIp(req) {
|
|
69
|
-
const forwarded = req.headers["x-forwarded-for"];
|
|
70
|
-
if (forwarded) {
|
|
71
|
-
const ip = Array.isArray(forwarded) ? forwarded[0] : forwarded.split(",")[0];
|
|
72
|
-
return ip?.trim() ?? "unknown";
|
|
73
|
-
}
|
|
74
|
-
const realIp = req.headers["x-real-ip"];
|
|
75
|
-
if (realIp) {
|
|
76
|
-
return Array.isArray(realIp) ? realIp[0] ?? "unknown" : realIp;
|
|
77
|
-
}
|
|
78
|
-
return req.socket?.remoteAddress ?? "unknown";
|
|
79
|
-
}
|
|
80
|
-
function parseUserAgent(userAgent) {
|
|
81
|
-
if (!userAgent) {
|
|
82
|
-
return { deviceType: void 0, browser: void 0, os: void 0 };
|
|
83
|
-
}
|
|
84
|
-
const ua = userAgent.toLowerCase();
|
|
85
|
-
let deviceType;
|
|
86
|
-
if (["mobile", "android", "iphone", "ipod"].some((x) => ua.includes(x))) {
|
|
87
|
-
deviceType = "mobile";
|
|
88
|
-
} else if (["ipad", "tablet"].some((x) => ua.includes(x))) {
|
|
89
|
-
deviceType = "tablet";
|
|
90
|
-
} else {
|
|
91
|
-
deviceType = "desktop";
|
|
92
|
-
}
|
|
93
|
-
let browser;
|
|
94
|
-
if (ua.includes("firefox")) browser = "Firefox";
|
|
95
|
-
else if (ua.includes("edg")) browser = "Edge";
|
|
96
|
-
else if (ua.includes("chrome")) browser = "Chrome";
|
|
97
|
-
else if (ua.includes("safari")) browser = "Safari";
|
|
98
|
-
else if (ua.includes("opera") || ua.includes("opr")) browser = "Opera";
|
|
99
|
-
let os;
|
|
100
|
-
if (ua.includes("windows")) os = "Windows";
|
|
101
|
-
else if (ua.includes("mac os") || ua.includes("macos")) os = "macOS";
|
|
102
|
-
else if (ua.includes("linux")) os = "Linux";
|
|
103
|
-
else if (ua.includes("android")) os = "Android";
|
|
104
|
-
else if (ua.includes("iphone") || ua.includes("ipad")) os = "iOS";
|
|
105
|
-
return { deviceType, browser, os };
|
|
106
|
-
}
|
|
107
|
-
var PocketPing = class {
|
|
108
|
-
constructor(config = {}) {
|
|
109
|
-
this.wss = null;
|
|
110
|
-
this.sessionSockets = /* @__PURE__ */ new Map();
|
|
111
|
-
this.operatorOnline = false;
|
|
112
|
-
this.config = config;
|
|
113
|
-
this.storage = this.initStorage(config.storage);
|
|
114
|
-
this.bridges = config.bridges ?? [];
|
|
115
|
-
}
|
|
116
|
-
initStorage(storage) {
|
|
117
|
-
if (!storage || storage === "memory") {
|
|
118
|
-
return new MemoryStorage();
|
|
119
|
-
}
|
|
120
|
-
return storage;
|
|
121
|
-
}
|
|
122
|
-
// ─────────────────────────────────────────────────────────────────
|
|
123
|
-
// Express/Connect Middleware
|
|
124
|
-
// ─────────────────────────────────────────────────────────────────
|
|
125
|
-
middleware() {
|
|
126
|
-
return async (req, res, next) => {
|
|
127
|
-
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
128
|
-
const path = url.pathname.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
129
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
130
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
131
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
132
|
-
if (req.method === "OPTIONS") {
|
|
133
|
-
res.statusCode = 204;
|
|
134
|
-
res.end();
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
try {
|
|
138
|
-
const body = await this.parseBody(req);
|
|
139
|
-
const query = Object.fromEntries(url.searchParams);
|
|
140
|
-
let result;
|
|
141
|
-
switch (path) {
|
|
142
|
-
case "connect": {
|
|
143
|
-
const connectReq = body;
|
|
144
|
-
const clientIp = getClientIp(req);
|
|
145
|
-
const userAgent = req.headers["user-agent"];
|
|
146
|
-
const uaInfo = parseUserAgent(connectReq.metadata?.userAgent ?? userAgent);
|
|
147
|
-
if (connectReq.metadata) {
|
|
148
|
-
connectReq.metadata.ip = clientIp;
|
|
149
|
-
connectReq.metadata.deviceType = connectReq.metadata.deviceType ?? uaInfo.deviceType;
|
|
150
|
-
connectReq.metadata.browser = connectReq.metadata.browser ?? uaInfo.browser;
|
|
151
|
-
connectReq.metadata.os = connectReq.metadata.os ?? uaInfo.os;
|
|
152
|
-
} else {
|
|
153
|
-
connectReq.metadata = {
|
|
154
|
-
ip: clientIp,
|
|
155
|
-
userAgent,
|
|
156
|
-
...uaInfo
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
result = await this.handleConnect(connectReq);
|
|
160
|
-
break;
|
|
161
|
-
}
|
|
162
|
-
case "message":
|
|
163
|
-
result = await this.handleMessage(body);
|
|
164
|
-
break;
|
|
165
|
-
case "messages":
|
|
166
|
-
result = await this.handleGetMessages(query);
|
|
167
|
-
break;
|
|
168
|
-
case "typing":
|
|
169
|
-
result = await this.handleTyping(body);
|
|
170
|
-
break;
|
|
171
|
-
case "presence":
|
|
172
|
-
result = await this.handlePresence();
|
|
173
|
-
break;
|
|
174
|
-
case "read":
|
|
175
|
-
result = await this.handleRead(body);
|
|
176
|
-
break;
|
|
177
|
-
default:
|
|
178
|
-
if (next) {
|
|
179
|
-
next();
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
res.statusCode = 404;
|
|
183
|
-
res.end(JSON.stringify({ error: "Not found" }));
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
res.setHeader("Content-Type", "application/json");
|
|
187
|
-
res.statusCode = 200;
|
|
188
|
-
res.end(JSON.stringify(result));
|
|
189
|
-
} catch (error) {
|
|
190
|
-
console.error("[PocketPing] Error:", error);
|
|
191
|
-
res.statusCode = 500;
|
|
192
|
-
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
async parseBody(req) {
|
|
197
|
-
if (req.body) return req.body;
|
|
198
|
-
return new Promise((resolve, reject) => {
|
|
199
|
-
let data = "";
|
|
200
|
-
req.on("data", (chunk) => data += chunk);
|
|
201
|
-
req.on("end", () => {
|
|
202
|
-
try {
|
|
203
|
-
resolve(data ? JSON.parse(data) : {});
|
|
204
|
-
} catch {
|
|
205
|
-
reject(new Error("Invalid JSON"));
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
req.on("error", reject);
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
// ─────────────────────────────────────────────────────────────────
|
|
212
|
-
// WebSocket
|
|
213
|
-
// ─────────────────────────────────────────────────────────────────
|
|
214
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
215
|
-
attachWebSocket(server) {
|
|
216
|
-
this.wss = new WebSocketServer({
|
|
217
|
-
server,
|
|
218
|
-
path: "/pocketping/stream"
|
|
219
|
-
});
|
|
220
|
-
this.wss.on("connection", (ws, req) => {
|
|
221
|
-
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
222
|
-
const sessionId = url.searchParams.get("sessionId");
|
|
223
|
-
if (!sessionId) {
|
|
224
|
-
ws.close(4e3, "sessionId required");
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
if (!this.sessionSockets.has(sessionId)) {
|
|
228
|
-
this.sessionSockets.set(sessionId, /* @__PURE__ */ new Set());
|
|
229
|
-
}
|
|
230
|
-
this.sessionSockets.get(sessionId).add(ws);
|
|
231
|
-
ws.on("close", () => {
|
|
232
|
-
this.sessionSockets.get(sessionId)?.delete(ws);
|
|
233
|
-
});
|
|
234
|
-
ws.on("message", async (data) => {
|
|
235
|
-
try {
|
|
236
|
-
const event = JSON.parse(data.toString());
|
|
237
|
-
await this.handleWebSocketMessage(sessionId, event);
|
|
238
|
-
} catch (err) {
|
|
239
|
-
console.error("[PocketPing] WS message error:", err);
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
async handleWebSocketMessage(sessionId, event) {
|
|
245
|
-
switch (event.type) {
|
|
246
|
-
case "typing":
|
|
247
|
-
this.broadcastToSession(sessionId, {
|
|
248
|
-
type: "typing",
|
|
249
|
-
data: event.data
|
|
250
|
-
});
|
|
251
|
-
break;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
broadcastToSession(sessionId, event) {
|
|
255
|
-
const sockets = this.sessionSockets.get(sessionId);
|
|
256
|
-
if (!sockets) return;
|
|
257
|
-
const message = JSON.stringify(event);
|
|
258
|
-
for (const ws of sockets) {
|
|
259
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
260
|
-
ws.send(message);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
// ─────────────────────────────────────────────────────────────────
|
|
265
|
-
// Protocol Handlers
|
|
266
|
-
// ─────────────────────────────────────────────────────────────────
|
|
267
|
-
async handleConnect(request) {
|
|
268
|
-
let session = null;
|
|
269
|
-
if (request.sessionId) {
|
|
270
|
-
session = await this.storage.getSession(request.sessionId);
|
|
271
|
-
}
|
|
272
|
-
if (!session && this.storage.getSessionByVisitorId) {
|
|
273
|
-
session = await this.storage.getSessionByVisitorId(request.visitorId);
|
|
274
|
-
}
|
|
275
|
-
if (!session) {
|
|
276
|
-
session = {
|
|
277
|
-
id: this.generateId(),
|
|
278
|
-
visitorId: request.visitorId,
|
|
279
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
280
|
-
lastActivity: /* @__PURE__ */ new Date(),
|
|
281
|
-
operatorOnline: this.operatorOnline,
|
|
282
|
-
aiActive: false,
|
|
283
|
-
metadata: request.metadata
|
|
284
|
-
};
|
|
285
|
-
await this.storage.createSession(session);
|
|
286
|
-
await this.notifyBridges("new_session", session);
|
|
287
|
-
await this.config.onNewSession?.(session);
|
|
288
|
-
} else if (request.metadata) {
|
|
289
|
-
if (session.metadata) {
|
|
290
|
-
request.metadata.ip = session.metadata.ip ?? request.metadata.ip;
|
|
291
|
-
request.metadata.country = session.metadata.country ?? request.metadata.country;
|
|
292
|
-
request.metadata.city = session.metadata.city ?? request.metadata.city;
|
|
293
|
-
}
|
|
294
|
-
session.metadata = request.metadata;
|
|
295
|
-
session.lastActivity = /* @__PURE__ */ new Date();
|
|
296
|
-
await this.storage.updateSession(session);
|
|
297
|
-
}
|
|
298
|
-
const messages = await this.storage.getMessages(session.id);
|
|
299
|
-
return {
|
|
300
|
-
sessionId: session.id,
|
|
301
|
-
visitorId: session.visitorId,
|
|
302
|
-
operatorOnline: this.operatorOnline,
|
|
303
|
-
welcomeMessage: this.config.welcomeMessage,
|
|
304
|
-
messages
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
async handleMessage(request) {
|
|
308
|
-
const session = await this.storage.getSession(request.sessionId);
|
|
309
|
-
if (!session) {
|
|
310
|
-
throw new Error("Session not found");
|
|
311
|
-
}
|
|
312
|
-
const message = {
|
|
313
|
-
id: this.generateId(),
|
|
314
|
-
sessionId: request.sessionId,
|
|
315
|
-
content: request.content,
|
|
316
|
-
sender: request.sender,
|
|
317
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
318
|
-
replyTo: request.replyTo
|
|
319
|
-
};
|
|
320
|
-
await this.storage.saveMessage(message);
|
|
321
|
-
session.lastActivity = /* @__PURE__ */ new Date();
|
|
322
|
-
await this.storage.updateSession(session);
|
|
323
|
-
if (request.sender === "visitor") {
|
|
324
|
-
await this.notifyBridges("message", message, session);
|
|
325
|
-
}
|
|
326
|
-
this.broadcastToSession(request.sessionId, {
|
|
327
|
-
type: "message",
|
|
328
|
-
data: message
|
|
329
|
-
});
|
|
330
|
-
await this.config.onMessage?.(message, session);
|
|
331
|
-
return {
|
|
332
|
-
messageId: message.id,
|
|
333
|
-
timestamp: message.timestamp.toISOString()
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
async handleGetMessages(request) {
|
|
337
|
-
const limit = Math.min(request.limit ?? 50, 100);
|
|
338
|
-
const messages = await this.storage.getMessages(request.sessionId, request.after, limit + 1);
|
|
339
|
-
return {
|
|
340
|
-
messages: messages.slice(0, limit),
|
|
341
|
-
hasMore: messages.length > limit
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
async handleTyping(request) {
|
|
345
|
-
this.broadcastToSession(request.sessionId, {
|
|
346
|
-
type: "typing",
|
|
347
|
-
data: {
|
|
348
|
-
sessionId: request.sessionId,
|
|
349
|
-
sender: request.sender,
|
|
350
|
-
isTyping: request.isTyping ?? true
|
|
351
|
-
}
|
|
352
|
-
});
|
|
353
|
-
return { ok: true };
|
|
354
|
-
}
|
|
355
|
-
async handlePresence() {
|
|
356
|
-
return {
|
|
357
|
-
online: this.operatorOnline,
|
|
358
|
-
aiEnabled: !!this.config.ai,
|
|
359
|
-
aiActiveAfter: this.config.aiTakeoverDelay ?? 300
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
async handleRead(request) {
|
|
363
|
-
const session = await this.storage.getSession(request.sessionId);
|
|
364
|
-
if (!session) {
|
|
365
|
-
throw new Error("Session not found");
|
|
366
|
-
}
|
|
367
|
-
const status = request.status ?? "read";
|
|
368
|
-
const now = /* @__PURE__ */ new Date();
|
|
369
|
-
let updated = 0;
|
|
370
|
-
const messages = await this.storage.getMessages(request.sessionId);
|
|
371
|
-
for (const msg of messages) {
|
|
372
|
-
if (request.messageIds.includes(msg.id)) {
|
|
373
|
-
msg.status = status;
|
|
374
|
-
if (status === "delivered") {
|
|
375
|
-
msg.deliveredAt = now;
|
|
376
|
-
} else if (status === "read") {
|
|
377
|
-
msg.deliveredAt = msg.deliveredAt ?? now;
|
|
378
|
-
msg.readAt = now;
|
|
379
|
-
}
|
|
380
|
-
await this.storage.saveMessage(msg);
|
|
381
|
-
updated++;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
this.broadcastToSession(request.sessionId, {
|
|
385
|
-
type: "read",
|
|
386
|
-
data: {
|
|
387
|
-
messageIds: request.messageIds,
|
|
388
|
-
status,
|
|
389
|
-
deliveredAt: status === "delivered" ? now.toISOString() : void 0,
|
|
390
|
-
readAt: status === "read" ? now.toISOString() : void 0
|
|
391
|
-
}
|
|
392
|
-
});
|
|
393
|
-
await this.notifyBridgesRead(request.sessionId, request.messageIds, status);
|
|
394
|
-
return { updated };
|
|
395
|
-
}
|
|
396
|
-
// ─────────────────────────────────────────────────────────────────
|
|
397
|
-
// Operator Actions (for bridges)
|
|
398
|
-
// ─────────────────────────────────────────────────────────────────
|
|
399
|
-
async sendOperatorMessage(sessionId, content) {
|
|
400
|
-
const response = await this.handleMessage({
|
|
401
|
-
sessionId,
|
|
402
|
-
content,
|
|
403
|
-
sender: "operator"
|
|
404
|
-
});
|
|
405
|
-
const message = {
|
|
406
|
-
id: response.messageId,
|
|
407
|
-
sessionId,
|
|
408
|
-
content,
|
|
409
|
-
sender: "operator",
|
|
410
|
-
timestamp: new Date(response.timestamp)
|
|
411
|
-
};
|
|
412
|
-
return message;
|
|
413
|
-
}
|
|
414
|
-
setOperatorOnline(online) {
|
|
415
|
-
this.operatorOnline = online;
|
|
416
|
-
for (const sessionId of this.sessionSockets.keys()) {
|
|
417
|
-
this.broadcastToSession(sessionId, {
|
|
418
|
-
type: "presence",
|
|
419
|
-
data: { online }
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
// ─────────────────────────────────────────────────────────────────
|
|
424
|
-
// Bridges
|
|
425
|
-
// ─────────────────────────────────────────────────────────────────
|
|
426
|
-
async notifyBridges(event, ...args) {
|
|
427
|
-
for (const bridge of this.bridges) {
|
|
428
|
-
try {
|
|
429
|
-
switch (event) {
|
|
430
|
-
case "new_session":
|
|
431
|
-
await bridge.onNewSession?.(args[0]);
|
|
432
|
-
break;
|
|
433
|
-
case "message":
|
|
434
|
-
await bridge.onMessage?.(args[0], args[1]);
|
|
435
|
-
break;
|
|
436
|
-
}
|
|
437
|
-
} catch (err) {
|
|
438
|
-
console.error(`[PocketPing] Bridge ${bridge.name} error:`, err);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
async notifyBridgesRead(sessionId, messageIds, status) {
|
|
443
|
-
for (const bridge of this.bridges) {
|
|
444
|
-
try {
|
|
445
|
-
await bridge.onMessageRead?.(sessionId, messageIds, status);
|
|
446
|
-
} catch (err) {
|
|
447
|
-
console.error(`[PocketPing] Bridge ${bridge.name} read notification error:`, err);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
addBridge(bridge) {
|
|
452
|
-
this.bridges.push(bridge);
|
|
453
|
-
bridge.init?.(this);
|
|
454
|
-
}
|
|
455
|
-
// ─────────────────────────────────────────────────────────────────
|
|
456
|
-
// Utilities
|
|
457
|
-
// ─────────────────────────────────────────────────────────────────
|
|
458
|
-
generateId() {
|
|
459
|
-
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 11)}`;
|
|
460
|
-
}
|
|
461
|
-
getStorage() {
|
|
462
|
-
return this.storage;
|
|
463
|
-
}
|
|
464
|
-
};
|
|
465
|
-
export {
|
|
466
|
-
MemoryStorage,
|
|
467
|
-
PocketPing
|
|
468
|
-
};
|