-
Notifications
You must be signed in to change notification settings - Fork 228
[WIP-GSoC-19] Store Livechat sessions on backend side #242
base: develop
Are you sure you want to change the base?
Changes from 15 commits
7c0c54e
55c34b9
65593b2
28675f9
8f37e07
a95236b
c91b895
00078ab
58c8423
dc1b2cc
7918686
bbdbd24
f8bb330
34cd17e
bf85c25
5281673
816e75f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,11 @@ | ||
import queryString from 'query-string'; | ||
import LivechatClient from '@rocket.chat/sdk/lib/clients/Livechat'; | ||
|
||
const host = window.SERVER_URL | ||
|| queryString.parse(window.location.search).serverUrl | ||
|| (process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : null); | ||
// eslint-disable-next-line import/no-absolute-path | ||
import LivechatClient from '/Users/knrt10/dev/openSource/RocketChat/Rocket.Chat.js.SDK/dist/lib/clients/Livechat'; | ||
|
||
const host = window.SERVER_URL | ||
|| queryString.parse(window.location.search).serverUrl | ||
|| (process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : null); | ||
const useSsl = host && host.match(/^https:/) !== null; | ||
|
||
export const Livechat = new LivechatClient({ host, protocol: 'ddp', useSsl }); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ import { Component } from 'preact'; | |
import { Router, route } from 'preact-router'; | ||
import queryString from 'query-string'; | ||
|
||
import { locationUpdate, userSessionPresence } from '../../lib/location'; | ||
import history from '../../history'; | ||
import Chat from '../../routes/Chat'; | ||
import LeaveMessage from '../../routes/LeaveMessage'; | ||
|
@@ -136,6 +137,7 @@ export class App extends Component { | |
async initialize() { | ||
// TODO: split these behaviors into composable components | ||
await Connection.init(); | ||
locationUpdate(); | ||
this.handleTriggers(); | ||
CustomFields.init(); | ||
Hooks.init(); | ||
|
@@ -149,6 +151,7 @@ export class App extends Component { | |
async finalize() { | ||
CustomFields.reset(); | ||
userPresence.reset(); | ||
userSessionPresence.reset(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't need this lib. |
||
visibility.removeListener(this.handleVisibilityChange); | ||
I18n.off('change', this.handleLanguageChange); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,290 @@ | ||
/* eslint-disable no-lonely-if */ | ||
/* eslint-disable no-alert */ | ||
import { getToken } from './main'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a clone from the |
||
import { Livechat } from '../api'; | ||
import store from '../store'; | ||
|
||
const docActivityEvents = ['mousemove', 'mousedown', 'touchend', 'keydown']; | ||
const token = getToken(); | ||
let timer; | ||
let initiated = false; | ||
const awayTime = 300000; | ||
let self; | ||
let oldStatus; | ||
|
||
export const userSessionPresence = { | ||
|
||
init() { | ||
if (initiated) { | ||
return; | ||
} | ||
|
||
initiated = true; | ||
self = this; | ||
store.on('change', this.handleStoreChange); | ||
}, | ||
|
||
reset() { | ||
initiated = false; | ||
this.stopEvents(); | ||
store.off('change', this.handleStoreChange); | ||
}, | ||
|
||
stopTimer() { | ||
timer && clearTimeout(timer); | ||
}, | ||
|
||
startTimer() { | ||
this.stopTimer(); | ||
timer = setTimeout(this.setAway, awayTime); | ||
}, | ||
|
||
handleStoreChange(state) { | ||
if (!initiated) { | ||
return; | ||
} | ||
|
||
const { token } = state; | ||
token ? self.startEvents() : self.stopEvents(); | ||
}, | ||
|
||
startEvents() { | ||
docActivityEvents.forEach((event) => { | ||
document.addEventListener(event, this.setOnline); | ||
}); | ||
|
||
window.addEventListener('focus', this.setOnline); | ||
}, | ||
|
||
stopEvents() { | ||
docActivityEvents.forEach((event) => { | ||
document.removeEventListener(event, this.setOnline); | ||
}); | ||
|
||
window.removeEventListener('focus', this.setOnline); | ||
this.stopTimer(); | ||
}, | ||
|
||
async setOnline() { | ||
self.startTimer(); | ||
if (oldStatus === 'online') { | ||
return; | ||
} | ||
oldStatus = 'online'; | ||
|
||
await Livechat.updateSessionStatus('online', token); | ||
}, | ||
|
||
async setAway() { | ||
self.stopTimer(); | ||
if (oldStatus === 'away') { | ||
return; | ||
} | ||
oldStatus = 'away'; | ||
await Livechat.updateSessionStatus('away', token); | ||
}, | ||
}; | ||
|
||
const deviceInfo = () => { | ||
const module = { | ||
options: [], | ||
header: [navigator.platform, navigator.userAgent, navigator.appVersion, navigator.vendor, window.opera], | ||
dataos: [ | ||
{ name: 'Windows Phone', value: 'Windows Phone', version: 'OS' }, | ||
{ name: 'Windows', value: 'Win', version: 'NT' }, | ||
{ name: 'iPhone', value: 'iPhone', version: 'OS' }, | ||
{ name: 'iPad', value: 'iPad', version: 'OS' }, | ||
{ name: 'Kindle', value: 'Silk', version: 'Silk' }, | ||
{ name: 'Android', value: 'Android', version: 'Android' }, | ||
{ name: 'PlayBook', value: 'PlayBook', version: 'OS' }, | ||
{ name: 'BlackBerry', value: 'BlackBerry', version: '/' }, | ||
{ name: 'Macintosh', value: 'Mac', version: 'OS X' }, | ||
{ name: 'Linux', value: 'Linux', version: 'rv' }, | ||
{ name: 'Palm', value: 'Palm', version: 'PalmOS' }, | ||
], | ||
databrowser: [ | ||
{ name: 'Chrome', value: 'Chrome', version: 'Chrome' }, | ||
{ name: 'Firefox', value: 'Firefox', version: 'Firefox' }, | ||
{ name: 'Safari', value: 'Safari', version: 'Version' }, | ||
{ name: 'Internet Explorer', value: 'MSIE', version: 'MSIE' }, | ||
{ name: 'Opera', value: 'Opera', version: 'Opera' }, | ||
{ name: 'BlackBerry', value: 'CLDC', version: 'CLDC' }, | ||
{ name: 'Mozilla', value: 'Mozilla', version: 'Mozilla' }, | ||
], | ||
init() { | ||
const agent = this.header.join(' '); | ||
const os = this.matchItem(agent, this.dataos); | ||
const browser = this.matchItem(agent, this.databrowser); | ||
|
||
return { os, browser }; | ||
}, | ||
matchItem(string, data) { | ||
let i = 0; | ||
let j = 0; | ||
let regex; | ||
let regexv; | ||
let match; | ||
let matches; | ||
let version; | ||
|
||
for (i = 0; i < data.length; i += 1) { | ||
regex = new RegExp(data[i].value, 'i'); | ||
match = regex.test(string); | ||
if (match) { | ||
regexv = new RegExp(`${ data[i].version }[- /:;]([\\d._]+)`, 'i'); | ||
matches = string.match(regexv); | ||
version = ''; | ||
if (matches) { if (matches[1]) { matches = matches[1]; } } | ||
if (matches) { | ||
matches = matches.split(/[._]+/); | ||
for (j = 0; j < matches.length; j += 1) { | ||
if (j === 0) { | ||
version += `${ matches[j] }.`; | ||
} else { | ||
version += matches[j]; | ||
} | ||
} | ||
} else { | ||
version = '0'; | ||
} | ||
return { | ||
name: data[i].name, | ||
version: parseFloat(version), | ||
}; | ||
} | ||
} | ||
return { name: 'unknown', version: 0 }; | ||
}, | ||
}; | ||
|
||
const info = module.init(); | ||
return { | ||
os: info.os.name, | ||
osVersion: info.os.version, | ||
browserName: info.browser.name, | ||
browserVersion: info.browser.version, | ||
}; | ||
}; | ||
|
||
const userDataWithoutLocation = { | ||
token, | ||
deviceInfo: deviceInfo(), | ||
}; | ||
|
||
|
||
/** | ||
* This is used to convert location to a default type we want to send to server | ||
* @param {Object} location | ||
* @returns {Object} | ||
*/ | ||
const convertLocationToSend = (location) => ( | ||
{ | ||
countryName: location.country || location.country_name, | ||
countryCode: location.country_code, | ||
city: location.city || location.state, | ||
latitude: location.latitude, | ||
longitude: location.longitude, | ||
completLocation: `${ location.country }, ${ location.state }, ${ location.city }`, | ||
}); | ||
|
||
/** | ||
* This is used to get location details for user | ||
* @param {Number} latitude | ||
* @param {Number} longitude | ||
* @returns {Object} | ||
*/ | ||
const locationPrimary = async (latitude, longitude) => { | ||
const { address } = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${ latitude }&lon=${ longitude }`, { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this is going out to this api to grab location? This has to definitely be optional if done at all. Some serious privacy concerns around sending data like this out. |
||
mode: 'cors', | ||
headers: { | ||
'Access-Control-Allow-Origin': '*', | ||
}, | ||
}).then((res) => res.json()); | ||
|
||
const location = convertLocationToSend(address); | ||
location.latitude = latitude; | ||
location.longitude = longitude; | ||
|
||
return { | ||
location, | ||
token, | ||
deviceInfo: deviceInfo(), | ||
}; | ||
}; | ||
|
||
/** | ||
* This is backup method to get location of user | ||
* @returns {Object} | ||
*/ | ||
const locationBackup = async () => { | ||
const location = await fetch('https://api.ipdata.co?api-key=test', { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here |
||
headers: { | ||
Accept: 'application/json', | ||
}, | ||
}).then((res) => res.json()); | ||
|
||
return { | ||
location: convertLocationToSend(location), | ||
token, | ||
deviceInfo: deviceInfo(), | ||
}; | ||
}; | ||
|
||
/** | ||
* This function works in following way | ||
* 1. Check user location already present or not | ||
* 2. If not, asks for user location access | ||
* 3. If not granted, sets locationAccess in store as false, so that we ask for access again, before starting chat | ||
* 4. If granted, sets location of user info to DB | ||
* 5. If location already present, increases the visit count for user | ||
*/ | ||
export const locationUpdate = async () => { | ||
const checkLocationUser = await Livechat.checkLocationUser(token); | ||
// check user location all ready there or not | ||
if (checkLocationUser && !checkLocationUser._id) { | ||
// Ask for permission for location | ||
if (navigator.geolocation) { | ||
store.setState({ | ||
locationAccess: true, | ||
}); | ||
navigator.geolocation.getCurrentPosition(async (position) => { | ||
const locationUser = await locationPrimary(position.coords.latitude, position.coords.longitude); | ||
await Livechat.sendLocationData(locationUser); | ||
userSessionPresence.init(); | ||
}, async (err) => { | ||
// This means user has denied location access | ||
// We need then to confirm location before starting the chat | ||
// Save state of location access inside store. | ||
if (err) { | ||
store.setState({ | ||
locationAccess: false, | ||
}); | ||
userSessionPresence.init(); | ||
// Send user data without location | ||
await Livechat.sendUserDataWithoutLocation(userDataWithoutLocation); | ||
} | ||
}); | ||
} else { | ||
// It means navigator is not supported in the browser, so ask | ||
// for location access by backup API. | ||
if (confirm('Please allow to access your location, for better assistance')) { | ||
store.setState({ | ||
locationAccess: true, | ||
}); | ||
const locationUser = await locationBackup(); | ||
await Livechat.sendLocationData(locationUser); | ||
userSessionPresence.init(); | ||
} else { | ||
store.setState({ | ||
locationAccess: false, | ||
}); | ||
userSessionPresence.init(); | ||
// Send user data without location | ||
await Livechat.sendUserDataWithoutLocation(userDataWithoutLocation); | ||
} | ||
} | ||
} else { | ||
// Update visit count for user | ||
Livechat.updateVisitCount(token); | ||
} | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -106,13 +106,15 @@ export class ChatContainer extends Component { | |
this.stopTypingDebounced.stop(); | ||
await Promise.all([ | ||
this.stopTyping({ rid, username: user.username }), | ||
Livechat.updateSessionStatus('online', token), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why call this method here? |
||
Livechat.sendMessage({ msg, token, rid }), | ||
]); | ||
} catch (error) { | ||
const { data: { error: reason } } = error; | ||
const alert = { id: createToken(), children: reason, error: true, timeout: 5000 }; | ||
await dispatch({ alerts: (alerts.push(alert), alerts) }); | ||
} | ||
|
||
await Livechat.notifyVisitorTyping(rid, user.username, false); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,6 +30,7 @@ export class RegisterContainer extends Component { | |
await dispatch({ loading: true, department }); | ||
try { | ||
await Livechat.grantVisitor({ visitor: { ...fields, token } }); | ||
await Livechat.updateVisitorSessionOnRegister({ visitor: { ...fields, token } }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not just one method to update the livechat session? |
||
parentCall('callback', ['pre-chat-form-submit', fields]); | ||
await loadConfig(); | ||
} finally { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you forget to remove this code before pushing your changes.