Skip to content
This repository has been archived by the owner on Apr 3, 2019. It is now read-only.

Commit

Permalink
Merge pull request #92 from Automattic/backlog
Browse files Browse the repository at this point in the history
Chat transcript API
  • Loading branch information
beaucollins committed Jan 17, 2017
2 parents cbd66f4 + 46e2e31 commit 8e3d6ae
Show file tree
Hide file tree
Showing 12 changed files with 189 additions and 78 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "happychat-service",
"version": "0.10.8",
"version": "0.11.0-2",
"description": "Socket.IO based chat server for happychat.",
"main": "index.js",
"scripts": {
Expand Down
47 changes: 47 additions & 0 deletions src/middleware-interface.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,48 @@
import isEmpty from 'lodash/isEmpty'
import assign from 'lodash/assign'

const debug = require( 'debug' )( 'happychat:middleware' )

const runMiddleware = middlewares => ( { origin, destination, chat, user, message } ) => new Promise( ( resolveMiddleware ) => {
new Promise( middlewareComplete => {
if ( isEmpty( middlewares ) ) {
debug( 'no middlewares registered' )
return middlewareComplete( message )
}
// copy the middleware
const context = middlewares.slice()
debug( 'running middleware', context.length )
// recursively run each middleware piping the result into
// the next middleware
const run = ( data, [ head, ... rest ] ) => {
if ( !head ) {
debug( 'middleware complete', chat.id, data.type )
return middlewareComplete( data.message )
}

// Wrapping call to middleware in Promise in case of exception
new Promise( resolve => resolve( head( data ) ) )
// continue running with remaining middleware
.then( nextMessage => run( assign( {}, data, { message: nextMessage } ), rest ) )
// if middleware fails, log the error and continue processing
.catch( e => {
debug( 'middleware failed to run', e )
debug( e.stack )
run( data, rest )
} )
}
// kick off the middleware processing
run( { origin, destination, chat, user, message }, context )
} )
.then( result => {
if ( ! result ) {
throw new Error( `middleware prevented message(id:${ message.id }) from being sent from ${ origin } to ${ destination } in chat ${ chat.id }` )
}
resolveMiddleware( result )
} )
.catch( e => debug( e.message, e ) )
} )

export default () => {
const middlewares = []
const external = {
Expand All @@ -15,3 +60,5 @@ export default () => {
external
}
}

export { runMiddleware as run }
3 changes: 2 additions & 1 deletion src/state/action-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ export const OPERATOR_LEAVE = 'OPERATOR_LEAVE';
export const OPERATOR_TYPING = 'OPERATOR_TYPING';
export const OPERATOR_CHAT_LEAVE = 'OPERATOR_CHAT_LEAVE';
export const OPERATOR_CHAT_JOIN = 'OPERATOR_CHAT_JOIN';
export const OPERATOR_CHAT_BACKLOG_REQUEST = 'OPERATOR_CHAT_BACKLOG_REQUEST';
export const OPERATOR_CHAT_TRANSCRIPT_REQUEST = 'OPERATOR_CHAT_TRANSCRIPT_REQUEST';
export const CUSTOMER_CHAT_TRANSCRIPT_REQUEST = 'CUSTOMER_CHAT_TRANSCRIPT_REQUEST';
export const OPERATOR_CHAT_TRANSFER = 'OPERATOR_CHAT_TRANSFER';
export const OPERATOR_READY = 'OPERATOR_READY'
export const SET_OPERATOR_CAPACITY = 'SET_OPERATOR_CAPACITY';
Expand Down
12 changes: 11 additions & 1 deletion src/state/chatlist/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ import {
CUSTOMER_DISCONNECT,
AUTOCLOSE_CHAT,
CUSTOMER_LEFT,
UPDATE_CHAT
UPDATE_CHAT,
RECEIVE_CHAT_MESSAGE,
CUSTOMER_CHAT_TRANSCRIPT_REQUEST
} from '../action-types'

export const reassignChats = ( operator, socket ) => ( {
Expand Down Expand Up @@ -142,6 +144,14 @@ export const autocloseChat = id => ( {
type: AUTOCLOSE_CHAT, id
} )

export const receiveMessage = ( origin, chat, message, user ) => ( {
type: RECEIVE_CHAT_MESSAGE, origin, chat, message, user
} )

export const updateChat = ( chat ) => ( {
type: UPDATE_CHAT, chat
} )

export const customerChatTranscriptRequest = ( chat, timestamp ) => ( {
type: CUSTOMER_CHAT_TRANSCRIPT_REQUEST, chat, timestamp
} )
4 changes: 2 additions & 2 deletions src/state/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ export default ( { io, customerAuth, operatorAuth, agentAuth, messageMiddlewares
logger,
delayedDispatch,
controllerMiddleware( messageMiddlewares ),
operatorMiddleware( io.of( '/operator' ), operatorAuth ),
operatorMiddleware( io.of( '/operator' ), operatorAuth, messageMiddlewares ),
agentMiddleware( io.of( '/agent' ), agentAuth ),
chatlistMiddleware( {
io,
timeout,
customerDisconnectTimeout: timeout,
customerDisconnectMessageTimeout: timeout
}, customerAuth ),
}, customerAuth, messageMiddlewares ),
broadcastMiddleware( io.of( '/operator' ), canRemoteDispatch ),
...operatorLoadMiddleware,
)
Expand Down
40 changes: 33 additions & 7 deletions src/state/middlewares/socket-io/chatlist.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
operatorJoinChat,
customerSocketDisconnect,
customerDisconnect,
customerChatTranscriptRequest,
customerLeft,
autocloseChat,
updateChat
Expand Down Expand Up @@ -80,6 +81,7 @@ import {
haveAvailableCapacity
} from '../../operator/selectors'
import { makeEventMessage, timestamp } from '../../util'
import { run } from '../../../middleware-interface'

const debug = require( 'debug' )( 'happychat:middleware:chatlist' )

Expand Down Expand Up @@ -109,11 +111,13 @@ const withTimeout = ( promise, ms = 1000 ) => Promise.race( [
} )
] )

const init = ( { user, socket, io, store, chat } ) => () => {
const init = ( { user, socket, io, store, chat }, middlewares ) => () => {
const runMiddleware = ( ... args ) => run( middlewares )( ... args )

socket.on( 'message', ( { text, id, meta } ) => {
const message = { session_id: chat.id, id: id, text, timestamp: timestamp(), user: identityForUser( user ), meta }
// all customer connections for this user receive the message
store.dispatch( customerInboundMessage( chat, message ) )
store.dispatch( customerInboundMessage( chat, message, user ) )
} )

socket.on( 'typing', ( text ) => {
Expand All @@ -127,19 +131,41 @@ const init = ( { user, socket, io, store, chat } ) => () => {
.then( () => store.dispatch( customerDisconnect( chat, user ) ) )
} )

socket.on( 'transcript', ( transcript_timestamp, callback ) => {
store.dispatch(
customerChatTranscriptRequest( chat, transcript_timestamp )
).then( result => new Promise( ( resolve, reject ) => {
Promise.all( map( message => runMiddleware( {
origin: message.source,
destination: 'customer',
user: message.user,
message,
chat
} ), result.messages ) )
.then(
messages => resolve( { timestamp: result.timestamp, messages } ),
reject
)
} ) )
.then(
result => callback( null, result ),
e => callback( e.message, null )
)
} )

socket.emit( 'init', user )
store.dispatch( customerJoin( socket, chat, user ) )
}

const join = ( { io, user, socket, store } ) => {
const join = ( { io, user, socket, store }, middlewares ) => {
const chat = {
user_id: user.id,
id: user.session_id,
username: user.username,
name: user.name,
name: user.displayName,
picture: user.picture
}
socket.join( customerRoom( chat.id ), init( { user, socket, io, store, chat } ) )
socket.join( customerRoom( chat.id ), init( { user, socket, io, store, chat }, middlewares ) )
}

const getClients = ( server, room ) => new Promise( ( resolve, reject ) => {
Expand All @@ -151,13 +177,13 @@ const getClients = ( server, room ) => new Promise( ( resolve, reject ) => {
} )
} )

export default ( { io, timeout = 1000, customerDisconnectTimeout = 90000, customerDisconnectMessageTimeout = 10000 }, customerAuth ) => store => {
export default ( { io, timeout = 1000, customerDisconnectTimeout = 90000, customerDisconnectMessageTimeout = 10000 }, customerAuth, middlewares = [] ) => store => {
const operator_io = io.of( '/operator' )
const customer_io = io.of( '/customer' )
.on( 'connection', socket => {
customerAuth( socket )
.then(
user => join( { socket, user, io: customer_io, store } ),
user => join( { socket, user, io: customer_io, store }, middlewares ),
e => debug( 'customer auth failed', e.message )
)
} )
Expand Down
87 changes: 30 additions & 57 deletions src/state/middlewares/socket-io/controller.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import isEmpty from 'lodash/isEmpty'
import assign from 'lodash/assign'
import get from 'lodash/get'
import set from 'lodash/set'
import { assoc } from 'ramda'
import { run } from '../../../middleware-interface'

import {
OPERATOR_TYPING,
Expand All @@ -17,7 +18,8 @@ import {
agentReceiveMessage,
customerReceiveMessage,
customerReceiveTyping,
operatorReceiveMessage
operatorReceiveMessage,
receiveMessage,
} from '../../chatlist/actions'

const debug = require( 'debug' )( 'happychat:controller' )
Expand Down Expand Up @@ -58,69 +60,32 @@ export class ChatLog {
}

// change a lib/customer message to what an agent client expects
const formatAgentMessage = ( author_type, author_id, session_id, { id, timestamp, text, meta, type } ) => ( {
const formatAgentMessage = ( author_type, author_id, session_id, { id, timestamp, text, meta, type, source } ) => ( {
id, timestamp, text,
session_id,
author_id,
author_type,
type,
meta
meta,
source
} )

export default ( middlewares ) => store => {
const log = { operator: new ChatLog(), customer: new ChatLog() }

const runMiddleware = ( { origin, destination, chat, user, message } ) => new Promise( ( resolveMiddleware ) => {
new Promise( middlewareComplete => {
if ( isEmpty( middlewares ) ) {
debug( 'no middlewares registered' )
return middlewareComplete( message )
}
// copy the middleware
const context = middlewares.slice()
debug( 'running middleware', context.length )
// recursively run each middleware piping the result into
// the next middleware
const run = ( data, [ head, ... rest ] ) => {
if ( !head ) {
debug( 'middleware complete', chat.id, data.type )
return middlewareComplete( data.message )
}

// Wrapping call to middleware in Promise in case of exception
new Promise( resolve => resolve( head( data ) ) )
// continue running with remaining middleware
.then( nextMessage => run( assign( {}, data, { message: nextMessage } ), rest ) )
// if middleware fails, log the error and continue processing
.catch( e => {
debug( 'middleware failed to run', e )
debug( e.stack )
run( data, rest )
} )
}
// kick off the middleware processing
run( { origin, destination, chat, user, message }, context )
} )
.then( result => {
if ( ! result ) {
throw new Error( `middleware prevented message(id:${ message.id }) from being sent from ${ origin } to ${ destination } in chat ${ chat.id }` )
}
resolveMiddleware( result )
} )
.catch( e => debug( e.message ) )
} )
const runMiddleware = ( ... args ) => run( middlewares )( ... args )

// toAgents( customers, 'disconnect', 'customer.disconnect' ) // TODO: do we want to wait till timer triggers?
const handleCustomerJoin = action => {
const { user, socket, chat } = action
const { socket, chat } = action
log.customer.findLog( chat.id )
.then( ( messages ) => {
socket.emit( 'log', messages )
} )
}

const handleOperatorJoin = action => {
const { chat, user: operator, socket } = action
const { chat, socket } = action
log.operator.findLog( chat.id )
.then( ( messages ) => {
socket.emit( 'log', chat, messages )
Expand All @@ -139,10 +104,13 @@ export default ( middlewares ) => store => {
}

const handleCustomerInboundMessage = ( action ) => {
const { chat, message } = action
const { chat, message, user } = action
// broadcast the message to
const customerMessage = assoc( 'source', 'customer', message );
store.dispatch( receiveMessage( 'customer', chat, customerMessage, user ) )

const origin = 'customer'
runMiddleware( { origin, destination: 'customer', chat, message } )
runMiddleware( { origin, destination: 'customer', chat, message: customerMessage } )
.then( m => new Promise( ( resolve, reject ) => {
log.customer.recordCustomerMessage( chat, m )
.then( () => resolve( m ), reject )
Expand All @@ -152,13 +120,13 @@ export default ( middlewares ) => store => {
) )
.catch( e => debug( 'middleware failed ', e.message ) )

runMiddleware( { origin, destination: 'agent', chat, message } )
runMiddleware( { origin, destination: 'agent', chat, message: customerMessage } )
.then( m => store.dispatch(
agentReceiveMessage( formatAgentMessage( 'customer', chat.id, chat.id, m ) ) )
)
.catch( e => debug( 'middleware failed', e.message ) )

runMiddleware( { origin, destination: 'operator', chat, message } )
runMiddleware( { origin, destination: 'operator', chat, message: customerMessage } )
.then( m => new Promise( ( resolve, reject ) => {
log.operator.recordCustomerMessage( chat, m )
.then( () => resolve( m ), reject )
Expand All @@ -169,24 +137,26 @@ export default ( middlewares ) => store => {

const handleOperatorInboundMessage = action => {
const { chat_id, user: operator, message } = action
// TODO: look up chat from store?
const operatorMessage = assoc( 'source', 'operator', message )
const chat = { id: chat_id }
debug( 'operator message', chat.id, message.id )
const origin = 'operator'

runMiddleware( { origin, destination: 'agent', chat, message, user: operator } )
store.dispatch( receiveMessage( 'operator', chat, operatorMessage, operator ) )

runMiddleware( { origin, destination: 'agent', chat, message: operatorMessage, user: operator } )
.then( m => store.dispatch(
agentReceiveMessage( formatAgentMessage( 'operator', operator.id, chat.id, m ) )
) )

runMiddleware( { origin, destination: 'operator', chat, message, user: operator } )
runMiddleware( { origin, destination: 'operator', chat, message: operatorMessage, user: operator } )
.then( m => new Promise( ( resolve, reject ) => {
log.operator.recordOperatorMessage( chat, operator, m )
.then( () => resolve( m ), reject )
} ) )
.then( m => store.dispatch( operatorReceiveMessage( chat.id, m ) ) )

runMiddleware( { origin, destination: 'customer', chat, message, user: operator } )
runMiddleware( { origin, destination: 'customer', chat, message: operatorMessage, user: operator } )
.then( m => new Promise( ( resolve, reject ) => {
log.customer.recordOperatorMessage( chat, operator, m )
.then( () => resolve( m ), reject )
Expand All @@ -197,24 +167,27 @@ export default ( middlewares ) => store => {
}

const handleAgentInboundMessage = action => {
const { message } = action
const { message, agent } = action
const chat = { id: message.session_id }
const format = ( m ) => assign( {}, { author_type: 'agent' }, m )
const agentMessage = assoc( 'source', 'agent', message )
const origin = 'agent'

runMiddleware( { origin, destination: 'agent', chat, message } )
store.dispatch( receiveMessage( 'agent', chat, agentMessage, agent ) )

runMiddleware( { origin, destination: 'agent', chat, message: agentMessage } )
.then( m => store.dispatch(
agentReceiveMessage( assign( {}, { author_type: 'agent' }, m ) ) )
)

runMiddleware( { origin, destination: 'operator', chat, message } )
runMiddleware( { origin, destination: 'operator', chat, message: agentMessage } )
.then( m => new Promise( ( resolve, reject ) => {
log.operator.recordAgentMessage( chat, m )
.then( () => resolve( m ), reject )
} ) )
.then( m => store.dispatch( operatorReceiveMessage( chat.id, format( m ) ) ) )

runMiddleware( { origin, destination: 'customer', chat, message } )
runMiddleware( { origin, destination: 'customer', chat, message: agentMessage } )
.then( m => new Promise( ( resolve, reject ) => {
log.customer.recordAgentMessage( chat, message )
.then( () => resolve( m ), reject )
Expand Down
Loading

0 comments on commit 8e3d6ae

Please sign in to comment.