console.info("===== Live Chat Proxy Entry - v16.3 ====="); const casettings = CAInterface_settings; const CA = CAInterface(); const msgrSettings = eval(`settings${casettings.hubSuffixMessenger}`); let nlu = msgrSettings?.nlu; const _SELF = `${nlu.apiBaseURL}ProxyScript/run/${req.params.workspaceId}/${req.params.branch}/CAInterface_chat${casettings.hubSuffix}`; // API Specific const headers = { Authorization: `Basic ${buffer.Buffer.from(casettings.token.basicAuth).toString("base64")}`, "Accept-Language": "en-US,en;q=0.5", }; const studioToken = msgrSettings ? { Authorization: `Bearer ${msgrSettings.messenger.token}` } : {}; console.info({ "Express Request": { body: req.body, }, }); const agentAltId = "G3YxOTO1LO_1NlJXEIfk"; // gather fields from request body const { conversationId, event, input, location, logged, options, queryParams, request, transcript } = req.body || {}; const channel = req?.body?.event?.metadata?.channel ?? req?.body?.channel ?? "web"; const metadata = req.body.event?.metadata; const connectedWithAgent = (session) => session?.liveChatRequest && session?.agent?.userId; const loopScale = 3; const smStr = `session-map-${conversationId}`; (async () => { const session = await redis.hGetAll(smStr); // parse session from string to object Object.keys(session).forEach((key) => { try { session[key] = JSON.parse(session[key]); } catch (e) {} }); const elapsed = (Date.now() - session?.lastActivity) / 1000; const qelapsed = (Date.now() - session?.lastQueueStatus) / 1000; console.info(`Time since last user activity: ${elapsed}s`); session.warned ||= false; if (!session?.discoveryUrls) { const { urls } = await CA.restUrls(); session.discoveryUrls = urls; await redis.hSet(smStr, { discoveryUrls: JSON.stringify(urls), }); } if (event != "FETCH") { console.info({ Session: session }); } const delayCallFetch = async (delay) => { const id = uuidv4(); await redis.hSet(smStr, { fetchid: id }); let numSeconds = typeof delay === "number" ? delay : loopScale; setTimeout(async (smStr, id) => { const session = await redis.hGetAll(smStr); if (session.fetchid == id) { axios({ method: "post", url: _SELF, headers: { "Content-Type": "application/json" }, data: { channel, conversationId, event: "FETCH" }, }); } else { console.info("Fetch: loop stopped (old id)"); } }, 1000 * numSeconds, smStr, id); }; switch (event) { case "JOIN": { console.info("Joining live chat session"); const chatRequestDefaults = { customerFirstName: "", customerLastName: "", customerEmail: "", chatLaunchMode: "CHAT_ONLY", refererURL: "example.com", launchIdentifier: casettings.liveChat.launchIdentifier, launchCode: "TextChatCustomerDataED", transcript, }; const customFieldsDefaults = { company: "Verint" }; const chatRequest = { ...chatRequestDefaults, ...(request ?? {}) }; chatRequest.customFields = { ...customFieldsDefaults, ...(request?.customFields ?? {}) }; const data = await CA.axios(`Posting data to "${session.discoveryUrls.client}"`, { method: "post", url: session.discoveryUrls.client, headers, data: chatRequest, }); // Clean up _links const links = {}; Object.keys(data._links).forEach((key) => { links[key] = data._links[key].href; }); data._links = links; console.info(data); await redis.hSet(smStr, { liveChatRequest: "true", chatDetails: JSON.stringify(data), lastActivity: JSON.stringify(Date.now()), lastQueueStatus: JSON.stringify(Date.now()), }); const queueStatus = await CA.axios(`Getting queue status from "${data._links.queuedstatus}"`, { method: "get", url: data._links.queuedstatus, headers }, async () => { await CA.postEvent(channel, conversationId, { input: casettings.liveChat.queueDisconnectMessage }); } ); if (queueStatus.message) { await CA.postEvent(channel, conversationId, { input: queueStatus.message }); } delayCallFetch(); break; } case "LEAVE": { console.info("Leaving live chat session"); try { const { data } = await axios({ method: "post", url: session.chatDetails._links.logout, headers, }); console.info({ "Logout Response": data }); if (connectedWithAgent(session)) { await CA.postConversationLeave( channel, conversationId, session.agent.userId || casettings.liveChat.participantId || agentAltId, "liveChatEnded" ); } } catch (e) { console.error(e); } console.info("reset session map"); await redis.hSet(smStr, { liveChatRequest: "false", agent: "{}" }); break; } case "FETCH": { const State = { waiting: 0, leaving: 1, offline: 2 }; let state = State.waiting; if (session.liveChatRequest == false) { console.info("Live Chat is off"); state = State.offline; } else if (session && session.chatDetails) { // queue status check if (!connectedWithAgent(session) && qelapsed > 30) { const queueStatus = await CA.axios( `Getting queue status from "${session.chatDetails._links.queuedstatus}"`, { method: "get", url: session.chatDetails._links.queuedstatus, headers }, async () => { await CA.postEvent(channel, conversationId, { input: casettings.liveChat.queueDisconnectMessage }); } ); if (queueStatus.message && queueStatus.message != session.lastQueueMessage) { await CA.postEvent(channel, conversationId, { input: queueStatus.message }); await redis.hSet(smStr, { lastQueueStatus: JSON.stringify(Date.now()), lastQueueMessage: queueStatus.message }); } } const data = await CA.axios( `Getting Events from "${session.chatDetails._links.event}"`, { method: "get", url: session.chatDetails._links.event, headers } ); data.events = data.events.map((event) => JSON.parse(event)); if (data.events.length > 0) console.info(data.events); const sagent = session.agent || {}; const au_default = { userId: casettings.liveChat.participantId || agentAltId, avatar: casettings.liveChat.avatar, type: "agent", }; let agent_user = { ...au_default, ...sagent }; for (const event of data.events) { const agentUser = { ...agent_user, name: event.userDisplayName }; if (event.userType === "GUEST") { console.info(event); switch (event.type) { case "MessageReceived": { console.info(`Guest said "${event.text}"`); await CA.postEvent(channel, conversationId, { conversationId, input: event.text, sentBy: agentUser, ping: nanoid(), sentAt: Date.now(), }); CA.addTransaction(session.conversationId, event.text, { type: "agent", name: event.userDisplayName, userId: event.userId }, { liveChat: true } ); break; } } } else if (event.userType === "AGENT") { console.info(event); switch (event.type) { case "UserTyping": await CA.postTyping(channel, conversationId); break; case "AttachmentMessageReceived": { let url = `${casettings.liveChat.attachment.endpoint}/ProxyScript/run/${req.params.workspaceId}/${req.params.branch}/CA_attachments${casettings.hubSuffix}`; url += `?url=${encodeURIComponent(event.url)}&sessionId=${event.sessionId}&fileName=${encodeURIComponent(event.fileName)}`; const response = await CA.downloadFile(event.url, event.sessionId); if (CA.channelIsExternal(channel) && response.headers['content-type']) { const attachment = { url, mimeType: response.headers['content-type'], fileName: event.fileName }; CA.postEvent(channel, conversationId, { metadata: { channel, attachments: [attachment] } }); } break; } case "MessageReceived": { console.info(`Agent ${event.userDisplayName} said "${event.text}"`); agent_user.name = event.userDisplayName; let eventPayload = { conversationId, input: event.text, sentBy: agentUser, ping: nanoid(), sentAt: Date.now(), }; if (data.events.some((x) => x.type == "UserExited")) { console.info("Setting liveChat to false"); eventPayload.metadata = { outputs: { liveChat: false } }; } await gvf_handleIVAWakeupEvent().handleIVAWakeupEvent( event, req, session, settings_66a1322d44405adda4fe9f53, redis, smStr, channel, eventPayload ); if (session.formFlow !== true) { await CA.postEvent(channel, conversationId, eventPayload); } CA.addTransaction( session.conversationId, event.text, { type: "agent", name: event.userDisplayName, userId: event.userID }, eventPayload?.metadata?.outputs || { liveChat: true } ); break; } case "UserEntered": console.info(`Agent ${event.userDisplayName} joined`); agent_user.name = event.userDisplayName; await redis.hSet(smStr, { agent: JSON.stringify(agent_user) }); await CA.postConversationJoin(channel, conversationId, agent_user); await CA.postEvent(channel, conversationId, { conversationId, input: `You are now connected with ${event.userDisplayName}`, sentBy: { ...agentUser, type: "host" }, ping: nanoid(), sentAt: Date.now(), }); break; case "UserExited": state = State.leaving; console.info(`Agent ${event.userDisplayName} left`); break; } } } } else { console.info("Session does not exist!"); break; } // State handling switch (state) { case State.waiting: { const idle = casettings.liveChat.idle; if (elapsed > idle.disconnect.seconds && session.warned) { CA.addTransaction(session.conversationId, "Live Chat Timeout", { type: "host" }, { liveChat: false }); await CA.postEvent(channel, conversationId, { input: idle.disconnect.message }); state = State.leaving; await axios({ method: "post", url: _SELF, headers: studioToken, data: { channel, conversationId, event: "LEAVE", session }, }); break; } else if (elapsed > idle.warning.seconds && !session.warned) { await CA.postEvent(channel, conversationId, { input: idle.warning.message }); await redis.hSet(smStr, { warned: "true" }); } delayCallFetch(); break; } case State.leaving: console.info("Leaving session"); await axios({ method: "post", url: _SELF, headers: studioToken, data: { channel, conversationId, event: "LEAVE", session }, }); break; default: state = State.offline; break; } break; } default: { console.info("Live chat ongoing"); if (input && session && session.chatDetails) { console.info(session.chatDetails); console.info(`Sending "${input}" to live chat`); await axios({ method: "post", url: session.chatDetails._links.event, headers, data: [{ type: "MessageSent", text: input }], }); await redis.hSet(smStr, { lastActivity: JSON.stringify(Date.now()), warned: "false", lastQueueMessage: "", lastQueueStatus: 0, }); delayCallFetch(); } } } })() .catch(async (error) => { console.error(`Caught error: ${error}`); if (error.response) console.error(`Caught error: ${error.response.data}`); const session = await redis.hGetAll(smStr); if (session) { await redis.hSet(smStr, { liveChatRequest: "false", agent: "{}" }); if (session && session.agent && session.chatDetails) { session.agent = JSON.parse(session.agent); session.chatDetails = JSON.parse(session.chatDetails); if (connectedWithAgent(session)) { CA.postConversationLeave( channel, conversationId, session.agent.userId || casettings.liveChat.participantId || agentAltId, "AgentExited" ); } axios({ method: "post", url: session.chatDetails._links.logout, headers }); } } if (conversationId && session.conversationId) { CA.addTransaction(session.conversationId, "Live Chat Error", { type: "host" }, { liveChat: false }); await CA.postEvent(channel, conversationId, { input: "There was a connection issue with the Live Chat, and you were disconnected.", }); } }) .finally(() => { res.send(200); });