2024-08-19 16:12:23 -03:00
import bodyParser from 'body-parser' ;
import express from 'express' ;
import fetch from 'isomorphic-fetch' ;
import moment from 'moment' ;
import * as uuidv4 from 'uuid' ;
import { IActivity , IBotData , IConversation , IConversationUpdateActivity , IMessageActivity } from './types' ;
const expiresIn = 1800 ;
const conversationsCleanupInterval = 10000 ;
const conversations : { [ key : string ] : IConversation } = { } ;
const botDataStore : { [ key : string ] : IBotData } = { } ;
2024-08-19 23:03:58 -03:00
export const getRouter = ( serviceUrl : string , botUrl : string , conversationInitRequired = true , botId ) : express . Router = > {
2024-08-19 16:12:23 -03:00
const router = express . Router ( ) ;
router . use ( bodyParser . json ( ) ) ; // for parsing application/json
router . use ( bodyParser . urlencoded ( { extended : true } ) ) ; // for parsing application/x-www-form-urlencoded
router . use ( ( req , res , next ) = > {
res . header ( 'Access-Control-Allow-Origin' , '*' ) ;
res . header ( 'Access-Control-Allow-Methods' , 'GET, PUT, POST, DELETE, PATCH, OPTIONS' ) ;
res . header ( 'Access-Control-Allow-Headers' , 'Origin, X-Requested-With, Content-Type, Accept, Authorization, x-ms-bot-agent' ) ;
next ( ) ;
} ) ;
2024-08-19 23:03:58 -03:00
2024-08-19 16:12:23 -03:00
// CLIENT ENDPOINT
2024-08-19 23:03:58 -03:00
router . options ( ` /directline/ ${ botId } / ` , ( req , res ) = > {
2024-08-19 16:12:23 -03:00
res . status ( 200 ) . end ( ) ;
} ) ;
// Creates a conversation
const reqs = ( req , res ) = > {
const conversationId : string = uuidv4 . v4 ( ) . toString ( ) ;
conversations [ conversationId ] = {
conversationId ,
history : [ ] ,
} ;
console . log ( 'Created conversation with conversationId: ' + conversationId ) ;
const activity = createConversationUpdateActivity ( serviceUrl , conversationId ) ;
fetch ( botUrl , {
method : 'POST' ,
body : JSON.stringify ( activity ) ,
headers : {
'Content-Type' : 'application/json' ,
} ,
} ) . then ( ( response ) = > {
res . status ( response . status ) . send ( {
conversationId ,
expiresIn ,
} ) ;
} ) ;
} ;
router . post ( '/v3/directline/conversations' , reqs ) ;
2024-08-19 23:03:58 -03:00
router . post ( ` /directline/ ${ botId } /conversations ` , reqs ) ;
2024-08-19 16:12:23 -03:00
// Reconnect API
router . get ( '/v3/directline/conversations/:conversationId' , ( req , res ) = > {
const conversation = getConversation ( req . params . conversationId , conversationInitRequired ) ;
if ( conversation ) {
res . status ( 200 ) . send ( conversation ) ;
} else {
// Conversation was never initialized
res . status ( 400 ) . send ( ) ;
}
console . warn ( '/v3/directline/conversations/:conversationId not implemented' ) ;
} ) ;
// Gets activities from store (local history array for now)
2024-08-19 23:03:58 -03:00
router . get ( ` /directline/ ${ botId } /conversations/:conversationId/activities ` , ( req , res ) = > {
2024-08-19 16:12:23 -03:00
const watermark = req . query . watermark && req . query . watermark !== 'null' ? Number ( req . query . watermark ) : 0 ;
const conversation = getConversation ( req . params . conversationId , conversationInitRequired ) ;
if ( conversation ) {
// If the bot has pushed anything into the history array
if ( conversation . history . length > watermark ) {
const activities = conversation . history . slice ( watermark ) ;
res . status ( 200 ) . json ( {
activities ,
watermark : watermark + activities . length ,
} ) ;
} else {
res . status ( 200 ) . send ( {
activities : [ ] ,
watermark ,
} ) ;
}
} else {
// Conversation was never initialized
res . status ( 400 ) . send ( ) ;
}
} ) ;
// Sends message to bot. Assumes message activities
2024-08-19 23:03:58 -03:00
router . post ( ` /directline/ ${ botId } /conversations/:conversationId/activities ` , ( req , res ) = > {
2024-08-19 16:12:23 -03:00
const incomingActivity = req . body ;
// Make copy of activity. Add required fields
const activity = createMessageActivity ( incomingActivity , serviceUrl , req . params . conversationId ) ;
const conversation = getConversation ( req . params . conversationId , conversationInitRequired ) ;
if ( conversation ) {
conversation . history . push ( activity ) ;
fetch ( botUrl , {
method : 'POST' ,
body : JSON.stringify ( activity ) ,
headers : {
'Content-Type' : 'application/json' ,
} ,
} ) . then ( ( response ) = > {
res . status ( response . status ) . json ( { id : activity.id } ) ;
} ) ;
} else {
// Conversation was never initialized
res . status ( 400 ) . send ( ) ;
}
} ) ;
router . post ( '/v3/directline/conversations/:conversationId/upload' , ( req , res ) = > { console . warn ( '/v3/directline/conversations/:conversationId/upload not implemented' ) ; } ) ;
router . get ( '/v3/directline/conversations/:conversationId/stream' , ( req , res ) = > { console . warn ( '/v3/directline/conversations/:conversationId/stream not implemented' ) ; } ) ;
// BOT CONVERSATION ENDPOINT
router . post ( '/v3/conversations' , ( req , res ) = > { console . warn ( '/v3/conversations not implemented' ) ; } ) ;
router . post ( '/v3/conversations/:conversationId/activities' , ( req , res ) = > {
let activity : IActivity ;
activity = req . body ;
activity . id = uuidv4 . v4 ( ) ;
activity . from = { id : 'id' , name : 'Bot' } ;
const conversation = getConversation ( req . params . conversationId , conversationInitRequired ) ;
if ( conversation ) {
conversation . history . push ( activity ) ;
res . status ( 200 ) . send ( ) ;
} else {
// Conversation was never initialized
res . status ( 400 ) . send ( ) ;
}
} ) ;
router . post ( '/v3/conversations/:conversationId/activities/:activityId' , ( req , res ) = > {
let activity : IActivity ;
activity = req . body ;
activity . id = uuidv4 . v4 ( ) ;
activity . from = { id : 'id' , name : 'Bot' } ;
const conversation = getConversation ( req . params . conversationId , conversationInitRequired ) ;
if ( conversation ) {
conversation . history . push ( activity ) ;
res . status ( 200 ) . send ( ) ;
} else {
// Conversation was never initialized
res . status ( 400 ) . send ( ) ;
}
} ) ;
router . get ( '/v3/conversations/:conversationId/members' , ( req , res ) = > { console . warn ( '/v3/conversations/:conversationId/members not implemented' ) ; } ) ;
router . get ( '/v3/conversations/:conversationId/activities/:activityId/members' , ( req , res ) = > { console . warn ( '/v3/conversations/:conversationId/activities/:activityId/members' ) ; } ) ;
// BOTSTATE ENDPOINT
router . get ( '/v3/botstate/:channelId/users/:userId' , ( req , res ) = > {
console . log ( 'Called GET user data' ) ;
getBotData ( req , res ) ;
} ) ;
router . get ( '/v3/botstate/:channelId/conversations/:conversationId' , ( req , res ) = > {
console . log ( ( 'Called GET conversation data' ) ) ;
getBotData ( req , res ) ;
} ) ;
router . get ( '/v3/botstate/:channelId/conversations/:conversationId/users/:userId' , ( req , res ) = > {
console . log ( 'Called GET private conversation data' ) ;
getBotData ( req , res ) ;
} ) ;
router . post ( '/v3/botstate/:channelId/users/:userId' , ( req , res ) = > {
console . log ( 'Called POST setUserData' ) ;
setUserData ( req , res ) ;
} ) ;
router . post ( '/v3/botstate/:channelId/conversations/:conversationId' , ( req , res ) = > {
console . log ( 'Called POST setConversationData' ) ;
setConversationData ( req , res ) ;
} ) ;
router . post ( '/v3/botstate/:channelId/conversations/:conversationId/users/:userId' , ( req , res ) = > {
setPrivateConversationData ( req , res ) ;
} ) ;
router . delete ( '/v3/botstate/:channelId/users/:userId' , ( req , res ) = > {
console . log ( 'Called DELETE deleteStateForUser' ) ;
deleteStateForUser ( req , res ) ;
} ) ;
return router ;
} ;
/ * *
* @param app The express app where your offline - directline endpoint will live
* @param port The port where your offline - directline will be hosted
* @param botUrl The url of the bot ( e . g . http : //127.0.0.1:3978/api/messages)
* @param conversationInitRequired Requires that a conversation is initialized before it is accessed , returning a 400
* when not the case . If set to false , a new conversation reference is created on the fly . This is true by default .
* /
2024-08-19 23:03:58 -03:00
export const initializeRoutes = ( app : express.Express , port : number , botUrl : string , conversationInitRequired = true , botId ) = > {
2024-08-19 16:12:23 -03:00
conversationsCleanup ( ) ;
const directLineEndpoint = ` http://127.0.0.1: ${ port } ` ;
2024-08-19 23:03:58 -03:00
const router = getRouter ( directLineEndpoint , botUrl , conversationInitRequired , botId ) ;
2024-08-19 16:12:23 -03:00
app . use ( router ) ;
console . log ( ` Routing messages to bot on ${ botUrl } ` ) ;
} ;
const getConversation = ( conversationId : string , conversationInitRequired : boolean ) = > {
// Create conversation on the fly when needed and init not required
if ( ! conversations [ conversationId ] && ! conversationInitRequired ) {
conversations [ conversationId ] = {
conversationId ,
history : [ ] ,
} ;
}
return conversations [ conversationId ] ;
} ;
const getBotDataKey = ( channelId : string , conversationId : string , userId : string ) = > {
return ` $ ${ channelId || '*' } ! ${ conversationId || '*' } ! ${ userId || '*' } ` ;
} ;
const setBotData = ( channelId : string , conversationId : string , userId : string , incomingData : IBotData ) : IBotData = > {
const key = getBotDataKey ( channelId , conversationId , userId ) ;
const newData : IBotData = {
eTag : new Date ( ) . getTime ( ) . toString ( ) ,
data : incomingData.data ,
} ;
if ( incomingData ) {
botDataStore [ key ] = newData ;
} else {
delete botDataStore [ key ] ;
newData . eTag = '*' ;
}
return newData ;
} ;
const getBotData = ( req : express.Request , res : express.Response ) = > {
const key = getBotDataKey ( req . params . channelId , req . params . conversationId , req . params . userId ) ;
console . log ( 'Data key: ' + key ) ;
res . status ( 200 ) . send ( botDataStore [ key ] || { data : null , eTag : '*' } ) ;
} ;
const setUserData = ( req : express.Request , res : express.Response ) = > {
res . status ( 200 ) . send ( setBotData ( req . params . channelId , req . params . conversationId , req . params . userId , req . body ) ) ;
} ;
const setConversationData = ( req : express.Request , res : express.Response ) = > {
res . status ( 200 ) . send ( setBotData ( req . params . channelId , req . params . conversationId , req . params . userId , req . body ) ) ;
} ;
const setPrivateConversationData = ( req : express.Request , res : express.Response ) = > {
res . status ( 200 ) . send ( setBotData ( req . params . channelId , req . params . conversationId , req . params . userId , req . body ) ) ;
} ;
2024-08-19 23:03:58 -03:00
export const start = ( server , botId ) = > {
2024-08-19 16:12:23 -03:00
2024-08-19 23:03:58 -03:00
initializeRoutes ( server , Number ( process . env . PORT ) , ` http://127.0.0.1: ${ process . env . PORT } /api/messages/ ${ botId } ` , null , botId ) ;
2024-08-19 16:12:23 -03:00
}
const deleteStateForUser = ( req : express.Request , res : express.Response ) = > {
Object . keys ( botDataStore )
. forEach ( ( key ) = > {
if ( key . endsWith ( ` !{req.query.userId} ` ) ) {
delete botDataStore [ key ] ;
}
} ) ;
res . status ( 200 ) . send ( ) ;
} ;
// CLIENT ENDPOINT HELPERS
const createMessageActivity = ( incomingActivity : IMessageActivity , serviceUrl : string , conversationId : string ) : IMessageActivity = > {
return { . . . incomingActivity , channelId : 'emulator' , serviceUrl , conversation : { id : conversationId } , id : uuidv4.v4 ( ) } ;
} ;
const createConversationUpdateActivity = ( serviceUrl : string , conversationId : string ) : IConversationUpdateActivity = > {
const activity : IConversationUpdateActivity = {
type : 'conversationUpdate' ,
channelId : 'emulator' ,
serviceUrl ,
conversation : { id : conversationId } ,
id : uuidv4.v4 ( ) ,
membersAdded : [ ] ,
membersRemoved : [ ] ,
from : { id : 'offline-directline' , name : 'Offline Directline Server' } ,
} ;
return activity ;
} ;
const conversationsCleanup = ( ) = > {
setInterval ( ( ) = > {
const expiresTime = moment ( ) . subtract ( expiresIn , 'seconds' ) ;
Object . keys ( conversations ) . forEach ( ( conversationId ) = > {
if ( conversations [ conversationId ] . history . length > 0 ) {
const lastTime = moment ( conversations [ conversationId ] . history [ conversations [ conversationId ] . history . length - 1 ] . localTimestamp ) ;
if ( lastTime < expiresTime ) {
delete conversations [ conversationId ] ;
console . log ( 'deleted cId: ' + conversationId ) ;
}
}
} ) ;
} , conversationsCleanupInterval ) ;
} ;