323 lines
12 KiB
TypeScript
323 lines
12 KiB
TypeScript
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 } = {};
|
|
|
|
export const getRouter = (serviceUrl: string, botUrl: string, conversationInitRequired = true, botId): express.Router => {
|
|
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();
|
|
});
|
|
|
|
// CLIENT ENDPOINT
|
|
router.options(`/directline/${botId}/`, (req, res) => {
|
|
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 );
|
|
router.post(`/directline/${botId}/conversations`,reqs );
|
|
|
|
// 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)
|
|
router.get(`/directline/${botId}/conversations/:conversationId/activities`, (req, res) => {
|
|
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
|
|
router.post(`/directline/${botId}/conversations/:conversationId/activities`, (req, res) => {
|
|
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.
|
|
*/
|
|
export const initializeRoutes = (app: express.Express, port: number, botUrl: string, conversationInitRequired = true, botId) => {
|
|
conversationsCleanup();
|
|
|
|
const directLineEndpoint = `http://127.0.0.1:${port}`;
|
|
const router = getRouter(directLineEndpoint, botUrl, conversationInitRequired, botId);
|
|
|
|
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));
|
|
};
|
|
|
|
export const start = (server, botId)=>{
|
|
|
|
initializeRoutes(server, Number(process.env.PORT), `http://127.0.0.1:${process.env.PORT}/api/messages/${botId}`, null, botId);
|
|
}
|
|
|
|
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);
|
|
};
|