Livechat SMS support (#2939)
* Add package to send SMS * Add support to livechat send and receive messages via SMS * Fix livechat typing indicator and first message after trigger * rename migration number.pull/2940/head
parent
d315b7b24c
commit
1fccfc229a
@ -0,0 +1,56 @@ |
||||
@Notifications = new class |
||||
constructor: -> |
||||
@debug = false |
||||
@streamAll = new Meteor.Streamer 'notify-all' |
||||
@streamRoom = new Meteor.Streamer 'notify-room' |
||||
@streamUser = new Meteor.Streamer 'notify-user' |
||||
|
||||
if @debug is true |
||||
@onAll -> console.log "RocketChat.Notifications: onAll", arguments |
||||
@onUser -> console.log "RocketChat.Notifications: onAll", arguments |
||||
|
||||
|
||||
notifyRoom: (room, eventName, args...) -> |
||||
console.log "RocketChat.Notifications: notifyRoom", arguments if @debug is true |
||||
|
||||
args.unshift "#{room}/#{eventName}" |
||||
@streamRoom.emit.apply @streamRoom, args |
||||
|
||||
notifyUser: (userId, eventName, args...) -> |
||||
console.log "RocketChat.Notifications: notifyUser", arguments if @debug is true |
||||
|
||||
args.unshift "#{userId}/#{eventName}" |
||||
@streamUser.emit.apply @streamUser, args |
||||
|
||||
notifyUsersOfRoom: (room, eventName, args...) -> |
||||
console.log "RocketChat.Notifications: notifyUsersOfRoom", arguments if @debug is true |
||||
|
||||
onlineUsers = RoomManager.onlineUsers.get() |
||||
room = ChatRoom.findOne(room) |
||||
for username in room?.usernames or [] |
||||
if onlineUsers[username]? |
||||
argsToSend = ["#{onlineUsers[username]._id}/#{eventName}"].concat args |
||||
@streamUser.emit.apply @streamUser, argsToSend |
||||
|
||||
|
||||
onAll: (eventName, callback) -> |
||||
@streamAll.on eventName, callback |
||||
|
||||
onRoom: (room, eventName, callback) -> |
||||
if @debug is true |
||||
@streamRoom.on room, -> console.log "RocketChat.Notifications: onRoom #{room}", arguments |
||||
|
||||
@streamRoom.on "#{room}/#{eventName}", callback |
||||
|
||||
onUser: (eventName, callback) -> |
||||
@streamUser.on "#{Meteor.userId()}/#{eventName}", callback |
||||
|
||||
|
||||
unAll: (callback) -> |
||||
@streamAll.removeListener 'notify', callback |
||||
|
||||
unRoom: (room, eventName, callback) -> |
||||
@streamRoom.removeListener "#{room}/#{eventName}", callback |
||||
|
||||
unUser: (callback) -> |
||||
@streamUser.removeListener Meteor.userId(), callback |
||||
@ -0,0 +1,54 @@ |
||||
/* globals Restivus */ |
||||
const Api = new Restivus({ |
||||
apiPath: 'livechat-api/', |
||||
useDefaultAuth: true, |
||||
prettyJson: true |
||||
}); |
||||
|
||||
Api.addRoute('sms-incoming/:service', { |
||||
post() { |
||||
const SMSService = RocketChat.SMS.getService(this.urlParams.service); |
||||
|
||||
const sms = SMSService.parse(this.bodyParams); |
||||
|
||||
var visitor = RocketChat.models.Users.findOneVisitorByPhone(sms.from); |
||||
|
||||
let sendMessage = { |
||||
message: { |
||||
_id: Random.id() |
||||
} |
||||
}; |
||||
|
||||
if (visitor) { |
||||
const rooms = RocketChat.models.Rooms.findByVisitorToken(visitor.profile.token).fetch(); |
||||
|
||||
if (rooms && rooms.length > 0) { |
||||
sendMessage.message.rid = rooms[0]._id; |
||||
} else { |
||||
sendMessage.message.rid = Random.id(); |
||||
} |
||||
sendMessage.message.token = visitor.profile.token; |
||||
} else { |
||||
sendMessage.message.rid = Random.id(); |
||||
sendMessage.message.token = Random.id(); |
||||
|
||||
let userId = RocketChat.Livechat.registerGuest({ |
||||
token: sendMessage.message.token, |
||||
phone: { |
||||
number: sms.from |
||||
} |
||||
}); |
||||
|
||||
visitor = RocketChat.models.Users.findOneById(userId); |
||||
|
||||
sendMessage.roomInfo = { |
||||
sms: true |
||||
}; |
||||
} |
||||
sendMessage.message.msg = sms.body; |
||||
|
||||
sendMessage.guest = visitor; |
||||
|
||||
return RocketChat.Livechat.sendMessage(sendMessage); |
||||
} |
||||
}); |
||||
@ -0,0 +1,121 @@ |
||||
RocketChat.Livechat = { |
||||
getNextAgent(department) { |
||||
if (department) { |
||||
return RocketChat.models.LivechatDepartmentAgents.getNextAgentForDepartment(department); |
||||
} else { |
||||
return RocketChat.models.Users.getNextAgent(); |
||||
} |
||||
}, |
||||
sendMessage({ guest, message, roomInfo }) { |
||||
var agent, room; |
||||
|
||||
room = RocketChat.models.Rooms.findOneById(message.rid); |
||||
if (room == null) { |
||||
|
||||
// if no department selected verify if there is only one active and use it
|
||||
if (!guest.department) { |
||||
var departments = RocketChat.models.LivechatDepartment.findEnabledWithAgents(); |
||||
if (departments.count() === 1) { |
||||
guest.department = departments.fetch()[0]._id; |
||||
} |
||||
} |
||||
|
||||
agent = RocketChat.Livechat.getNextAgent(guest.department); |
||||
if (!agent) { |
||||
throw new Meteor.Error('no-agent-online', 'Sorry, no online agents'); |
||||
} |
||||
let roomData = _.extend({ |
||||
_id: message.rid, |
||||
name: guest.username, |
||||
msgs: 1, |
||||
lm: new Date(), |
||||
usernames: [agent.username, guest.username], |
||||
t: 'l', |
||||
ts: new Date(), |
||||
v: { |
||||
token: message.token |
||||
} |
||||
}, roomInfo); |
||||
let subscriptionData = { |
||||
rid: message.rid, |
||||
name: guest.username, |
||||
alert: true, |
||||
open: true, |
||||
unread: 1, |
||||
answered: false, |
||||
u: { |
||||
_id: agent.agentId, |
||||
username: agent.username |
||||
}, |
||||
t: 'l', |
||||
desktopNotifications: 'all', |
||||
mobilePushNotifications: 'all', |
||||
emailNotifications: 'all' |
||||
}; |
||||
|
||||
RocketChat.models.Rooms.insert(roomData); |
||||
RocketChat.models.Subscriptions.insert(subscriptionData); |
||||
} |
||||
room = Meteor.call('canAccessRoom', message.rid, guest._id); |
||||
if (!room) { |
||||
throw new Meteor.Error('cannot-acess-room'); |
||||
} |
||||
return RocketChat.sendMessage(guest, message, room); |
||||
}, |
||||
registerGuest({ token, name, email, department, phone, loginToken } = {}) { |
||||
check(token, String); |
||||
|
||||
const user = RocketChat.models.Users.getVisitorByToken(token, { fields: { _id: 1 } }); |
||||
|
||||
if (user) { |
||||
throw new Meteor.Error('token-already-exists', 'Token already exists'); |
||||
} |
||||
|
||||
const username = RocketChat.models.Users.getNextVisitorUsername(); |
||||
|
||||
var userData = { |
||||
username: username, |
||||
globalRoles: ['livechat-guest'], |
||||
department: department, |
||||
type: 'visitor' |
||||
}; |
||||
|
||||
if (this.connection) { |
||||
userData.userAgent = this.connection.httpHeaders['user-agent']; |
||||
userData.ip = this.connection.httpHeaders['x-real-ip'] || this.connection.clientAddress; |
||||
userData.host = this.connection.httpHeaders.host; |
||||
} |
||||
|
||||
const userId = Accounts.insertUserDoc({}, userData); |
||||
|
||||
let updateUser = { |
||||
name: name || username, |
||||
profile: { |
||||
guest: true, |
||||
token: token |
||||
} |
||||
}; |
||||
|
||||
if (phone) { |
||||
updateUser.profile.phones = [ phone ]; |
||||
} |
||||
|
||||
if (email && email.trim() !== '') { |
||||
updateUser.emails = [{ address: email }]; |
||||
} |
||||
|
||||
if (loginToken) { |
||||
updateUser.services = { |
||||
resume: { |
||||
loginTokens: [ loginToken ] |
||||
} |
||||
}; |
||||
} |
||||
|
||||
Meteor.users.update(userId, { |
||||
$set: updateUser |
||||
}); |
||||
|
||||
return userId; |
||||
} |
||||
}; |
||||
@ -1,9 +0,0 @@ |
||||
/* exported getNextAgent */ |
||||
|
||||
this.getNextAgent = function(department) { |
||||
if (department) { |
||||
return RocketChat.models.LivechatDepartmentAgents.getNextAgentForDepartment(department); |
||||
} else { |
||||
return RocketChat.models.Users.getNextAgent(); |
||||
} |
||||
}; |
||||
@ -0,0 +1,37 @@ |
||||
RocketChat.callbacks.add('afterSaveMessage', function(message, room) { |
||||
// skips this callback if the message was edited
|
||||
if (message.editedAt) { |
||||
return message; |
||||
} |
||||
|
||||
if (!RocketChat.SMS.enabled) { |
||||
return message; |
||||
} |
||||
|
||||
// only send the sms by SMS if it is a livechat room with SMS set to true
|
||||
if (typeof room.t === 'undefined' || room.t !== 'l' || !room.sms || !room.v || !room.v.token) { |
||||
return message; |
||||
} |
||||
|
||||
// if the message has a token, it was sent from the visitor, so ignore it
|
||||
if (message.token) { |
||||
return message; |
||||
} |
||||
|
||||
const SMSService = RocketChat.SMS.getService(RocketChat.settings.get('SMS_Service')); |
||||
|
||||
if (!SMSService) { |
||||
return message; |
||||
} |
||||
|
||||
const visitor = RocketChat.models.Users.getVisitorByToken(room.v.token); |
||||
|
||||
if (!visitor || !visitor.profile || !visitor.profile.phones || visitor.profile.phones.length === 0) { |
||||
return message; |
||||
} |
||||
|
||||
SMSService.send(visitor.profile.phones[0].number, message.msg); |
||||
|
||||
return message; |
||||
|
||||
}, RocketChat.callbacks.priority.LOW); |
||||
@ -0,0 +1 @@ |
||||
node_modules |
||||
@ -0,0 +1,7 @@ |
||||
This directory and the files immediately inside it are automatically generated |
||||
when you change this package's NPM dependencies. Commit the files in this |
||||
directory (npm-shrinkwrap.json, .gitignore, and this README) to source control |
||||
so that others run the same versions of sub-dependencies. |
||||
|
||||
You should NOT check in the node_modules directory that Meteor automatically |
||||
creates; if you are using git, the .gitignore file tells git to ignore it. |
||||
@ -0,0 +1,312 @@ |
||||
{ |
||||
"dependencies": { |
||||
"twilio": { |
||||
"version": "2.9.1", |
||||
"dependencies": { |
||||
"request": { |
||||
"version": "2.55.0", |
||||
"dependencies": { |
||||
"bl": { |
||||
"version": "0.9.5", |
||||
"dependencies": { |
||||
"readable-stream": { |
||||
"version": "1.0.34", |
||||
"dependencies": { |
||||
"core-util-is": { |
||||
"version": "1.0.2" |
||||
}, |
||||
"isarray": { |
||||
"version": "0.0.1" |
||||
}, |
||||
"string_decoder": { |
||||
"version": "0.10.31" |
||||
}, |
||||
"inherits": { |
||||
"version": "2.0.1" |
||||
} |
||||
} |
||||
} |
||||
} |
||||
}, |
||||
"caseless": { |
||||
"version": "0.9.0" |
||||
}, |
||||
"forever-agent": { |
||||
"version": "0.6.1" |
||||
}, |
||||
"form-data": { |
||||
"version": "0.2.0", |
||||
"dependencies": { |
||||
"async": { |
||||
"version": "0.9.2" |
||||
} |
||||
} |
||||
}, |
||||
"json-stringify-safe": { |
||||
"version": "5.0.1" |
||||
}, |
||||
"mime-types": { |
||||
"version": "2.0.14", |
||||
"dependencies": { |
||||
"mime-db": { |
||||
"version": "1.12.0" |
||||
} |
||||
} |
||||
}, |
||||
"node-uuid": { |
||||
"version": "1.4.7" |
||||
}, |
||||
"qs": { |
||||
"version": "2.4.2" |
||||
}, |
||||
"tunnel-agent": { |
||||
"version": "0.4.2" |
||||
}, |
||||
"tough-cookie": { |
||||
"version": "2.2.2" |
||||
}, |
||||
"http-signature": { |
||||
"version": "0.10.1", |
||||
"dependencies": { |
||||
"assert-plus": { |
||||
"version": "0.1.5" |
||||
}, |
||||
"asn1": { |
||||
"version": "0.1.11" |
||||
}, |
||||
"ctype": { |
||||
"version": "0.5.3" |
||||
} |
||||
} |
||||
}, |
||||
"oauth-sign": { |
||||
"version": "0.6.0" |
||||
}, |
||||
"hawk": { |
||||
"version": "2.3.1", |
||||
"dependencies": { |
||||
"hoek": { |
||||
"version": "2.16.3" |
||||
}, |
||||
"boom": { |
||||
"version": "2.10.1" |
||||
}, |
||||
"cryptiles": { |
||||
"version": "2.0.5" |
||||
}, |
||||
"sntp": { |
||||
"version": "1.0.9" |
||||
} |
||||
} |
||||
}, |
||||
"aws-sign2": { |
||||
"version": "0.5.0" |
||||
}, |
||||
"stringstream": { |
||||
"version": "0.0.5" |
||||
}, |
||||
"combined-stream": { |
||||
"version": "0.0.7", |
||||
"dependencies": { |
||||
"delayed-stream": { |
||||
"version": "0.0.5" |
||||
} |
||||
} |
||||
}, |
||||
"isstream": { |
||||
"version": "0.1.2" |
||||
}, |
||||
"har-validator": { |
||||
"version": "1.8.0", |
||||
"dependencies": { |
||||
"bluebird": { |
||||
"version": "2.10.2" |
||||
}, |
||||
"chalk": { |
||||
"version": "1.1.3", |
||||
"dependencies": { |
||||
"ansi-styles": { |
||||
"version": "2.2.1" |
||||
}, |
||||
"escape-string-regexp": { |
||||
"version": "1.0.5" |
||||
}, |
||||
"has-ansi": { |
||||
"version": "2.0.0", |
||||
"dependencies": { |
||||
"ansi-regex": { |
||||
"version": "2.0.0" |
||||
} |
||||
} |
||||
}, |
||||
"strip-ansi": { |
||||
"version": "3.0.1", |
||||
"dependencies": { |
||||
"ansi-regex": { |
||||
"version": "2.0.0" |
||||
} |
||||
} |
||||
}, |
||||
"supports-color": { |
||||
"version": "2.0.0" |
||||
} |
||||
} |
||||
}, |
||||
"commander": { |
||||
"version": "2.9.0", |
||||
"dependencies": { |
||||
"graceful-readlink": { |
||||
"version": "1.0.1" |
||||
} |
||||
} |
||||
}, |
||||
"is-my-json-valid": { |
||||
"version": "2.13.1", |
||||
"dependencies": { |
||||
"generate-function": { |
||||
"version": "2.0.0" |
||||
}, |
||||
"generate-object-property": { |
||||
"version": "1.2.0", |
||||
"dependencies": { |
||||
"is-property": { |
||||
"version": "1.0.2" |
||||
} |
||||
} |
||||
}, |
||||
"jsonpointer": { |
||||
"version": "2.0.0" |
||||
}, |
||||
"xtend": { |
||||
"version": "4.0.1" |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
}, |
||||
"underscore": { |
||||
"version": "1.8.3" |
||||
}, |
||||
"jsonwebtoken": { |
||||
"version": "5.4.1", |
||||
"dependencies": { |
||||
"jws": { |
||||
"version": "3.1.3", |
||||
"dependencies": { |
||||
"base64url": { |
||||
"version": "1.0.6", |
||||
"dependencies": { |
||||
"concat-stream": { |
||||
"version": "1.4.10", |
||||
"dependencies": { |
||||
"inherits": { |
||||
"version": "2.0.1" |
||||
}, |
||||
"typedarray": { |
||||
"version": "0.0.6" |
||||
}, |
||||
"readable-stream": { |
||||
"version": "1.1.14", |
||||
"dependencies": { |
||||
"core-util-is": { |
||||
"version": "1.0.2" |
||||
}, |
||||
"isarray": { |
||||
"version": "0.0.1" |
||||
}, |
||||
"string_decoder": { |
||||
"version": "0.10.31" |
||||
} |
||||
} |
||||
} |
||||
} |
||||
}, |
||||
"meow": { |
||||
"version": "2.0.0", |
||||
"dependencies": { |
||||
"camelcase-keys": { |
||||
"version": "1.0.0", |
||||
"dependencies": { |
||||
"camelcase": { |
||||
"version": "1.2.1" |
||||
}, |
||||
"map-obj": { |
||||
"version": "1.0.1" |
||||
} |
||||
} |
||||
}, |
||||
"indent-string": { |
||||
"version": "1.2.2", |
||||
"dependencies": { |
||||
"get-stdin": { |
||||
"version": "4.0.1" |
||||
}, |
||||
"repeating": { |
||||
"version": "1.1.3", |
||||
"dependencies": { |
||||
"is-finite": { |
||||
"version": "1.0.1", |
||||
"dependencies": { |
||||
"number-is-nan": { |
||||
"version": "1.0.0" |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
}, |
||||
"minimist": { |
||||
"version": "1.2.0" |
||||
}, |
||||
"object-assign": { |
||||
"version": "1.0.0" |
||||
} |
||||
} |
||||
} |
||||
} |
||||
}, |
||||
"jwa": { |
||||
"version": "1.1.3", |
||||
"dependencies": { |
||||
"buffer-equal-constant-time": { |
||||
"version": "1.0.1" |
||||
}, |
||||
"ecdsa-sig-formatter": { |
||||
"version": "1.0.5", |
||||
"dependencies": { |
||||
"base64-url": { |
||||
"version": "1.2.2" |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
}, |
||||
"ms": { |
||||
"version": "0.7.1" |
||||
} |
||||
} |
||||
}, |
||||
"jwt-simple": { |
||||
"version": "0.1.0" |
||||
}, |
||||
"q": { |
||||
"version": "0.9.7" |
||||
}, |
||||
"scmp": { |
||||
"version": "0.0.3" |
||||
}, |
||||
"deprecate": { |
||||
"version": "0.1.0" |
||||
}, |
||||
"string.prototype.startswith": { |
||||
"version": "0.2.0" |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,23 @@ |
||||
/* globals RocketChat */ |
||||
RocketChat.SMS = { |
||||
enabled: false, |
||||
services: {}, |
||||
accountSid: null, |
||||
authToken: null, |
||||
fromNumber: null, |
||||
|
||||
registerService(name, service) { |
||||
this.services[name] = service; |
||||
}, |
||||
|
||||
getService(name) { |
||||
if (!this.services[name]) { |
||||
throw new Meteor.Error('error-sms-service-not-configured'); |
||||
} |
||||
return new this.services[name](this.accountSid, this.authToken, this.fromNumber); |
||||
} |
||||
}; |
||||
|
||||
RocketChat.settings.get('SMS_Enabled', function(key, value) { |
||||
RocketChat.SMS.enabled = value; |
||||
}); |
||||
@ -0,0 +1,21 @@ |
||||
Package.describe({ |
||||
name: 'rocketchat:sms', |
||||
version: '0.0.1', |
||||
summary: '', |
||||
git: '', |
||||
documentation: 'README.md' |
||||
}); |
||||
|
||||
Package.onUse(function(api) { |
||||
api.versionsFrom('1.2.1'); |
||||
api.use('ecmascript'); |
||||
api.use('rocketchat:lib'); |
||||
|
||||
api.addFiles('settings.js', 'server'); |
||||
api.addFiles('SMS.js', 'server'); |
||||
api.addFiles('services/twilio.js', 'server'); |
||||
}); |
||||
|
||||
Npm.depends({ |
||||
'twilio': '2.9.1' |
||||
}); |
||||
@ -0,0 +1,37 @@ |
||||
/* globals RocketChat */ |
||||
class Twilio { |
||||
constructor() { |
||||
this.accountSid = RocketChat.settings.get('SMS_Twilio_Account_SID'); |
||||
this.authToken = RocketChat.settings.get('SMS_Twilio_authToken'); |
||||
this.fromNumber = RocketChat.settings.get('SMS_Twilio_fromNumber'); |
||||
} |
||||
parse(data) { |
||||
return { |
||||
from: data.From, |
||||
to: data.To, |
||||
body: data.Body, |
||||
|
||||
extra: { |
||||
toCountry: data.ToCountry, |
||||
toState: data.ToState, |
||||
toCity: data.ToCity, |
||||
toZip: data.ToZip, |
||||
fromCountry: data.FromCountry, |
||||
fromState: data.FromState, |
||||
fromCity: data.FromCity, |
||||
fromZip: data.FromZip |
||||
} |
||||
}; |
||||
} |
||||
send(to, message) { |
||||
var client = Npm.require('twilio')(this.accountSid, this.authToken); |
||||
|
||||
client.messages.create({ |
||||
to: to, |
||||
from: this.fromNumber, |
||||
body: message, |
||||
}); |
||||
} |
||||
} |
||||
|
||||
RocketChat.SMS.registerService('twilio', Twilio); |
||||
@ -0,0 +1,43 @@ |
||||
Meteor.startup(function() { |
||||
RocketChat.settings.addGroup('SMS', function() { |
||||
this.add('SMS_Enabled', false, { |
||||
type: 'boolean' |
||||
}); |
||||
|
||||
this.add('SMS_Service', 'twilio', { |
||||
type: 'select', |
||||
values: [{ |
||||
key: 'twilio', |
||||
i18nLabel: 'Twilio' |
||||
}], |
||||
i18nLabel: 'Service' |
||||
}); |
||||
|
||||
this.section('Twilio', function() { |
||||
this.add('SMS_Twilio_fromNumber', '', { |
||||
type: 'string', |
||||
enableQuery: { |
||||
_id: 'SMS_Service', |
||||
value: 'twilio' |
||||
}, |
||||
i18nLabel: 'From_Number' |
||||
}); |
||||
this.add('SMS_Twilio_Account_SID', '', { |
||||
type: 'string', |
||||
enableQuery: { |
||||
_id: 'SMS_Service', |
||||
value: 'twilio' |
||||
}, |
||||
i18nLabel: 'Account_SID' |
||||
}); |
||||
this.add('SMS_Twilio_authToken', '', { |
||||
type: 'string', |
||||
enableQuery: { |
||||
_id: 'SMS_Service', |
||||
value: 'twilio' |
||||
}, |
||||
i18nLabel: 'Auth_Token' |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,14 @@ |
||||
RocketChat.Migrations.add({ |
||||
version: 45, |
||||
up: function() { |
||||
|
||||
// finds the latest created visitor
|
||||
var lastVisitor = RocketChat.models.Users.find({ type: 'visitor' }, { fields: { username: 1 }, sort: { createdAt: -1 }, limit: 1 }).fetch(); |
||||
|
||||
if (lastVisitor && lastVisitor.length > 0) { |
||||
var lastNumber = lastVisitor[0].username.replace(/^guest\-/, ''); |
||||
|
||||
RocketChat.settings.add('Livechat_guest_count' , (parseInt(lastNumber) + 1), { type: 'int', group: 'Livechat' }); |
||||
} |
||||
} |
||||
}); |
||||
Loading…
Reference in new issue