@meetelise/chat 1.20.84 → 1.20.86
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/package.json +3 -1
- package/public/demo/index.html +8 -2
- package/public/dist/index.js +507 -239
- package/public/dist/index.js.LICENSE.txt +17 -0
- package/src/MyPubnub.ts +319 -0
- package/src/WebComponent/me-chat.ts +195 -71
- package/src/WebComponent/pubnub-chat-styles.ts +185 -0
- package/src/WebComponent/pubnub-chat.ts +165 -0
- package/src/fetchFeatureFlag.ts +21 -0
- package/src/svgIcons.ts +9 -0
|
@@ -10,6 +10,23 @@ object-assign
|
|
|
10
10
|
http://jedwatson.github.io/classnames
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
/*! *****************************************************************************
|
|
14
|
+
Copyright (c) Microsoft Corporation.
|
|
15
|
+
|
|
16
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
17
|
+
purpose with or without fee is hereby granted.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
20
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
21
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
22
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
23
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
24
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
25
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
26
|
+
***************************************************************************** */
|
|
27
|
+
|
|
28
|
+
/*! lil-uuid - v0.1 - MIT License - https://github.com/lil-js/uuid */
|
|
29
|
+
|
|
13
30
|
/**
|
|
14
31
|
* @license
|
|
15
32
|
* Copyright 2017 Google LLC
|
package/src/MyPubnub.ts
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { AxiosError } from "axios";
|
|
2
|
+
import Pubnub, { ListenerParameters, MessageEvent } from "pubnub";
|
|
3
|
+
|
|
4
|
+
import axios from "axios";
|
|
5
|
+
import { Building } from "./fetchBuildingInfo";
|
|
6
|
+
import parseISO from "date-fns/parseISO";
|
|
7
|
+
import { v4 as uuid } from "uuid";
|
|
8
|
+
import addHours from "date-fns/addHours";
|
|
9
|
+
import isAfter from "date-fns/isAfter";
|
|
10
|
+
import formatISO from "date-fns/formatISO";
|
|
11
|
+
import isBefore from "date-fns/isBefore";
|
|
12
|
+
|
|
13
|
+
interface TokenResponse {
|
|
14
|
+
auth: {
|
|
15
|
+
result: {
|
|
16
|
+
token: string;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
keys: {
|
|
20
|
+
subscribe_key: string;
|
|
21
|
+
publish_key: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ChatMessage {
|
|
26
|
+
channel: string;
|
|
27
|
+
message: {
|
|
28
|
+
text: string;
|
|
29
|
+
customType: string;
|
|
30
|
+
};
|
|
31
|
+
publisher: string;
|
|
32
|
+
subscription: string;
|
|
33
|
+
timetoken: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ChatInfo {
|
|
37
|
+
leadId: string | null;
|
|
38
|
+
timestamp: Date | null;
|
|
39
|
+
buildingSlug: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class MyPubnub {
|
|
43
|
+
private apiHost = "https://app.meetelise.com";
|
|
44
|
+
|
|
45
|
+
private building: Building | null = null;
|
|
46
|
+
private buildingSlug: string;
|
|
47
|
+
private ttlHours = 24;
|
|
48
|
+
|
|
49
|
+
pubnub: Pubnub | null = null;
|
|
50
|
+
leadUserId = "";
|
|
51
|
+
channel = "";
|
|
52
|
+
|
|
53
|
+
chatListener:
|
|
54
|
+
| ((res: { messages: ChatMessage[]; isLoading: boolean }) => void)
|
|
55
|
+
| null = null;
|
|
56
|
+
|
|
57
|
+
messages: ChatMessage[] = [];
|
|
58
|
+
listenerParams: ListenerParameters = {
|
|
59
|
+
message: (messageEvent: MessageEvent) => {
|
|
60
|
+
this.messages = [
|
|
61
|
+
...this.messages,
|
|
62
|
+
{
|
|
63
|
+
channel: messageEvent.channel,
|
|
64
|
+
message: messageEvent.message,
|
|
65
|
+
publisher: messageEvent.publisher,
|
|
66
|
+
subscription: messageEvent.subscription,
|
|
67
|
+
timetoken: +messageEvent.timetoken,
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
const isWaitingForEliseResponse = messageEvent.publisher !== "eliseai";
|
|
71
|
+
this.chatListener?.({
|
|
72
|
+
messages: this.messages,
|
|
73
|
+
isLoading: isWaitingForEliseResponse,
|
|
74
|
+
});
|
|
75
|
+
this.isLoadingMessages = isWaitingForEliseResponse;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
isLoadingMessages = false;
|
|
79
|
+
|
|
80
|
+
constructor(buildingSlug: string, buildingDetails: Building) {
|
|
81
|
+
this.buildingSlug = buildingSlug;
|
|
82
|
+
this.building = buildingDetails;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
addChatListener(
|
|
86
|
+
listener: (response: {
|
|
87
|
+
messages: ChatMessage[];
|
|
88
|
+
isLoading: boolean;
|
|
89
|
+
}) => void
|
|
90
|
+
): void {
|
|
91
|
+
this.chatListener = listener;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async initializePubnub(): Promise<Pubnub | undefined> {
|
|
95
|
+
const storedChatKeyValues = this.getChatStorageKey();
|
|
96
|
+
if (!storedChatKeyValues.leadId) {
|
|
97
|
+
// eslint-disable-next-line no-console
|
|
98
|
+
console.error("Error getting chat storage key...");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.leadUserId = storedChatKeyValues.leadId;
|
|
103
|
+
this.channel = `webchat_${this.leadUserId}`;
|
|
104
|
+
|
|
105
|
+
// If the user comes back to the page after closing it out, we get a new token to persist
|
|
106
|
+
// the chat for another this.ttlHours hours.
|
|
107
|
+
if (
|
|
108
|
+
storedChatKeyValues.timestamp &&
|
|
109
|
+
isAfter(
|
|
110
|
+
storedChatKeyValues.timestamp,
|
|
111
|
+
addHours(new Date(), this.ttlHours)
|
|
112
|
+
)
|
|
113
|
+
) {
|
|
114
|
+
this.createChatStorageKey(this.leadUserId);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const pubnubToken = await this.fetchToken(this.leadUserId, this.channel);
|
|
118
|
+
|
|
119
|
+
// These keys are OK to expose live, the authKey generated by the BE is what
|
|
120
|
+
// is used to authenticate the user. Ideally, should also add rate limiting
|
|
121
|
+
// and/or IP whitelisting to the BE endpoint that generates the token.
|
|
122
|
+
this.pubnub = new Pubnub({
|
|
123
|
+
publishKey: pubnubToken.keys.publish_key,
|
|
124
|
+
subscribeKey: pubnubToken.keys.subscribe_key,
|
|
125
|
+
userId: this.leadUserId,
|
|
126
|
+
authKey: pubnubToken.auth.result.token,
|
|
127
|
+
});
|
|
128
|
+
this.withAuthToken(() => new Promise(() => this.handleChatListeners()));
|
|
129
|
+
await this.withAuthToken(() => this.getChannelHistory());
|
|
130
|
+
return this.pubnub;
|
|
131
|
+
}
|
|
132
|
+
async fetchToken(lead: string, channel: string): Promise<TokenResponse> {
|
|
133
|
+
const response = await axios.get(
|
|
134
|
+
`${this.apiHost}/platformApi/webchat/pn/request-token?user_id=${lead}&channel=${channel}`
|
|
135
|
+
);
|
|
136
|
+
return response.data;
|
|
137
|
+
}
|
|
138
|
+
async fetchChannelExists(channel: string): Promise<boolean> {
|
|
139
|
+
const response = await axios.get(
|
|
140
|
+
`${this.apiHost}/platformApi/webchat/check-channel-exists?channel_name=${channel}`
|
|
141
|
+
);
|
|
142
|
+
return response.data;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async withAuthToken(apiRequestFunc: () => Promise<void>): Promise<void> {
|
|
146
|
+
try {
|
|
147
|
+
await apiRequestFunc();
|
|
148
|
+
} catch (error: unknown) {
|
|
149
|
+
// only want to retry with new token if the error is a 403
|
|
150
|
+
if (
|
|
151
|
+
error instanceof AxiosError &&
|
|
152
|
+
error &&
|
|
153
|
+
error.response &&
|
|
154
|
+
error.response.status === 403
|
|
155
|
+
) {
|
|
156
|
+
try {
|
|
157
|
+
if (!this.pubnub || !this.leadUserId || !this.channel) return;
|
|
158
|
+
|
|
159
|
+
const newToken = await this.fetchToken(this.leadUserId, this.channel);
|
|
160
|
+
if (!newToken) return;
|
|
161
|
+
|
|
162
|
+
this.pubnub.setAuthKey(newToken.auth.result.token);
|
|
163
|
+
|
|
164
|
+
await apiRequestFunc();
|
|
165
|
+
} catch (retryError) {
|
|
166
|
+
//onsole.error(retryError);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async getChannelHistory(): Promise<void> {
|
|
173
|
+
try {
|
|
174
|
+
const response: Pubnub.FetchMessagesResponse = await new Promise(
|
|
175
|
+
(resolve, reject) => {
|
|
176
|
+
if (!this.pubnub || !this.channel) return;
|
|
177
|
+
this.pubnub.fetchMessages(
|
|
178
|
+
{
|
|
179
|
+
channels: [this.channel],
|
|
180
|
+
count: 100,
|
|
181
|
+
},
|
|
182
|
+
(status, response) => {
|
|
183
|
+
if (status.error) {
|
|
184
|
+
reject(status);
|
|
185
|
+
} else {
|
|
186
|
+
resolve(response);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
if (this.channel && Object.keys(response.channels).length !== 0) {
|
|
194
|
+
const currentChannelMessages = response.channels[this.channel];
|
|
195
|
+
const parsedCurrentChannelMessages: ChatMessage[] = [];
|
|
196
|
+
currentChannelMessages.forEach((message) => {
|
|
197
|
+
if (message.uuid) {
|
|
198
|
+
parsedCurrentChannelMessages.push({
|
|
199
|
+
channel: message.channel,
|
|
200
|
+
message: message.message,
|
|
201
|
+
publisher: message.uuid,
|
|
202
|
+
subscription: message.channel,
|
|
203
|
+
timetoken: +message.timetoken,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
this.messages = parsedCurrentChannelMessages;
|
|
208
|
+
this.chatListener?.({ messages: this.messages, isLoading: false });
|
|
209
|
+
}
|
|
210
|
+
} catch (error) {
|
|
211
|
+
// Handle the error here
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
handleChatListeners = (): void => {
|
|
216
|
+
if (!this.pubnub || !this.channel) return;
|
|
217
|
+
this.pubnub.subscribe({ channels: [this.channel] });
|
|
218
|
+
this.pubnub.addListener(this.listenerParams);
|
|
219
|
+
};
|
|
220
|
+
removeChatListeners = (): void => {
|
|
221
|
+
if (this.pubnub && this.channel) {
|
|
222
|
+
this.pubnub.unsubscribe({ channels: [this.channel] });
|
|
223
|
+
this.pubnub.removeListener(this.listenerParams);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
sendMessage = async (message: string): Promise<void> => {
|
|
228
|
+
if (message) {
|
|
229
|
+
if (!this.pubnub) {
|
|
230
|
+
// ONLY create/gets a chat session if user actually wants to chat
|
|
231
|
+
const myPubnub = await this.initializePubnub();
|
|
232
|
+
if (!myPubnub) return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
await this.withAuthToken(async () => {
|
|
236
|
+
if (!this.pubnub || !this.channel) return;
|
|
237
|
+
await this.pubnub.publish({
|
|
238
|
+
channel: this.channel,
|
|
239
|
+
message: {
|
|
240
|
+
text: message,
|
|
241
|
+
customType: "lead_message",
|
|
242
|
+
buildingId: this.building?.id,
|
|
243
|
+
buildingSlug: this.buildingSlug,
|
|
244
|
+
userId: this.building?.userId, // this userid is actually the AI user!
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
if (this.isLoadingMessages === false) this.isLoadingMessages = true;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
isLeadMessage = (message: ChatMessage): boolean =>
|
|
252
|
+
message.publisher.includes("lead_") &&
|
|
253
|
+
message.message.customType === "lead_message";
|
|
254
|
+
|
|
255
|
+
createChatStorageKey = (existingUserId?: string): ChatInfo => {
|
|
256
|
+
const storageTimestamp = formatISO(new Date());
|
|
257
|
+
const leadUserId = existingUserId ?? `lead_${uuid()}_${this.buildingSlug}`;
|
|
258
|
+
localStorage.setItem(
|
|
259
|
+
"com.eliseai.webchat.slug=" + this.buildingSlug,
|
|
260
|
+
JSON.stringify({
|
|
261
|
+
buildingSlug: this.buildingSlug,
|
|
262
|
+
leadId: leadUserId,
|
|
263
|
+
timestamp: storageTimestamp,
|
|
264
|
+
})
|
|
265
|
+
);
|
|
266
|
+
return {
|
|
267
|
+
leadId: leadUserId,
|
|
268
|
+
timestamp: parseISO(storageTimestamp),
|
|
269
|
+
buildingSlug: this.buildingSlug,
|
|
270
|
+
};
|
|
271
|
+
};
|
|
272
|
+
getChatStorageKey = (): ChatInfo => {
|
|
273
|
+
const eliseaiLocalStorageValue = localStorage.getItem(
|
|
274
|
+
"com.eliseai.webchat.slug=" + this.buildingSlug
|
|
275
|
+
);
|
|
276
|
+
if (eliseaiLocalStorageValue) {
|
|
277
|
+
try {
|
|
278
|
+
const eliseaiLocalStorageValueParsed = JSON.parse(
|
|
279
|
+
eliseaiLocalStorageValue
|
|
280
|
+
);
|
|
281
|
+
const lsBuildingSlug = eliseaiLocalStorageValueParsed.buildingSlug;
|
|
282
|
+
const lsLeadId = eliseaiLocalStorageValueParsed.leadId;
|
|
283
|
+
const lsExpiration = new Date(eliseaiLocalStorageValueParsed.timestamp);
|
|
284
|
+
|
|
285
|
+
if (
|
|
286
|
+
this.isChatKeyValid({
|
|
287
|
+
leadId: lsLeadId,
|
|
288
|
+
timestamp: lsExpiration,
|
|
289
|
+
buildingSlug: lsBuildingSlug,
|
|
290
|
+
})
|
|
291
|
+
)
|
|
292
|
+
return {
|
|
293
|
+
leadId: lsLeadId,
|
|
294
|
+
timestamp: lsExpiration,
|
|
295
|
+
buildingSlug: lsBuildingSlug,
|
|
296
|
+
};
|
|
297
|
+
} catch (_) {
|
|
298
|
+
// eslint-disable-next-line no-console
|
|
299
|
+
console.warn("Error getting chat storage key");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return this.createChatStorageKey();
|
|
304
|
+
};
|
|
305
|
+
isChatKeyValid = (storageValueDeconstructed: ChatInfo): boolean => {
|
|
306
|
+
if (
|
|
307
|
+
storageValueDeconstructed.buildingSlug !== this.buildingSlug ||
|
|
308
|
+
!storageValueDeconstructed.leadId ||
|
|
309
|
+
!storageValueDeconstructed.timestamp
|
|
310
|
+
) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const expirationDate = addHours(new Date(), this.ttlHours);
|
|
315
|
+
return isBefore(storageValueDeconstructed.timestamp, expirationDate);
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export default MyPubnub;
|