Skip to content

Commit

Permalink
feat: initial working prototype
Browse files Browse the repository at this point in the history
  • Loading branch information
Frank Gu committed Jan 31, 2021
1 parent eaf91c9 commit db925c7
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 25 deletions.
3 changes: 2 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"runtimeExecutable": "node",
"env": {
"NODE_ENV": "local",
"NODE_TLS_REJECT_UNAUTHORIZED": "0"
"NODE_TLS_REJECT_UNAUTHORIZED": "0",
"LOG_LEVEL": "trace"
},
"runtimeArgs": [
"--nolazy",
Expand Down
1 change: 1 addition & 0 deletions config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const CONFIG: Config = {
PORT: getProcessEnv('PORT'),

GENERAL_RUNTIME_ENDPOINT: getProcessEnv('GENERAL_RUNTIME_ENDPOINT'),
MESSENGER_API_ENDPOINT: getProcessEnv('MESSENGER_API_ENDPOINT'),
VERIFY_TOKEN: getProcessEnv('VERIFY_TOKEN'),

// Release information
Expand Down
11 changes: 9 additions & 2 deletions lib/clients/static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@ import axios from 'axios';
import { Config } from '@/types';

export default (config: Config) => {
axios.defaults.baseURL = config.GENERAL_RUNTIME_ENDPOINT;
const runtimeAxios = axios.create({
baseURL: config.GENERAL_RUNTIME_ENDPOINT,
});

const messengerAxios = axios.create({
baseURL: config.MESSENGER_API_ENDPOINT,
});
return {
axios,
runtimeAxios,
messengerAxios,
};
};
4 changes: 2 additions & 2 deletions lib/controllers/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class WebhookController extends AbstractController {
}

// Iterates over each entry - there may be multiple if batched
body.entry.forEach((entry: any) => {
body.entry.forEach(async (entry: any) => {
// Gets the body of the webhook event
const webhookEvent = entry.messaging[0];

Expand All @@ -57,7 +57,7 @@ class WebhookController extends AbstractController {
// SenderID -> Who is interacting with the app?
// AppID -> What is the target "skill"?
// Message -> Text
return webhookManager.handleMessage(webhookEvent.sender.id, webhookEvent.recipient.id, webhookEvent.message.text);
await webhookManager.handleMessage(webhookEvent.sender.id, webhookEvent.recipient.id, webhookEvent.message.text);
});

return 'EVENT_RECEIVED';
Expand Down
101 changes: 81 additions & 20 deletions lib/services/webhookManager.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,52 @@
import { AbstractManager } from '../utils';

class WebhookManager extends AbstractManager {
async handleMessage(senderID: string, appID: string, message: string): Promise<any[]> {
const { axios } = this.clients;
const versionID = this.getVersionID(appID);
const state = this.getState(senderID, appID);
async handleMessage(senderID: string, appID: string, message: string): Promise<any> {
const { runtimeAxios } = this.clients;
const versionID = await this.getVersionID(appID);
const state = await this.getState(senderID, appID);
const request = {
type: 'text',
payload: message,
};

// Make call to general-runtime
const runtimeReq = {
state,
request,
};
const result = (await axios.post(`/interact/${versionID}`, runtimeReq)) as any;
const retArr: any[] = [];
result.trace.forEach((trace: any) => {
const result = (await runtimeAxios.post(`/interact/${versionID}`, runtimeReq)) as any;
await this.saveState(senderID, appID, result.data.state);
const token = await this.getPageToken(appID);
result.data.trace.forEach(async (trace: any) => {
if (trace.type === 'speak' && trace.payload.type === 'message') {
retArr.push({
text: trace.payload.message,
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
try {
const cleanText = WebhookManager.stripSSML(trace.payload.message);
const sendResult = await this.callSendAPI(cleanText, senderID, token);
// eslint-disable-next-line no-console
console.log(sendResult);
} catch (err) {
// eslint-disable-next-line no-console
console.log(err);
}
} else if (trace.type === 'end') {
this.clearState(senderID, appID);
}
});
return retArr;
}

async getVersionID(_appID: string) {
// TODO: Call state store to get the messenger app/versionID map
return '6000dcbcc668b50006399b9a';
return 'ok';
}

async newSession(appID: string) {
const { axios } = this.clients;
const versionID = this.getVersionID(appID);
const { runtimeAxios } = this.clients;
const diagramID = await this.getDiagramID(appID);
const versionID = await this.getVersionID(appID);
const initiateSession = {
state: {
stack: [
{
diagramID: this.getVersionID(appID),
diagramID,
storage: {},
variables: {},
},
Expand All @@ -56,8 +64,8 @@ class WebhookManager extends AbstractManager {
};

// POST to general-runtime
const { state } = (await axios.post(`/interact/${versionID}`, initiateSession)) as any;
return state;
const result = (await runtimeAxios.post(`/interact/${versionID}`, initiateSession)) as any;
return result.data.state;
}

// Fetch the conversation state from persistence
Expand All @@ -73,6 +81,59 @@ class WebhookManager extends AbstractManager {
}
return result;
}

async saveState(senderID: string, appID: string, state: any) {
const { kvstore } = this.clients;
const stateKey = `${appID}-${senderID}`;
return kvstore.set(stateKey, state);
}

// Clear the state info if the conversation has reached the end
async clearState(senderID: string, appID: string) {
const { kvstore } = this.clients;
const stateKey = `${appID}-${senderID}`;
await kvstore.delete(stateKey);
}

async callSendAPI(text: string, recipient: string, pageAccessToken: string) {
const { messengerAxios } = this.clients;
const reqBody = {
message_type: 'RESPONSE',
recipient: {
id: recipient,
},
message: {
text,
},
};
return messengerAxios.post('/me/messages', reqBody, {
params: {
access_token: pageAccessToken,
},
});
}

static stripSSML(input: string) {
const regex = /(<([^>]+)>)/gi;
return input.replace(regex, '');
}

// TODO: The function stubs below should be replaced by actual config persistence for production.
// The program VersionID, diagramID, and the FB Messenger page tokens can be obtined during the initial "account linking" process.
async getVersionID(_appID: string) {
// TODO: Call state store to get the messenger app/versionID map
return 'some_version_id';
}

async getDiagramID(_appID: string) {
// TODO: Get a proper state store for this
return 'some_diagram_id';
}

async getPageToken(_appID: string) {
// TODO: persist page token
return 'some_page_token';
}
}

export default WebhookManager;
1 change: 1 addition & 0 deletions types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface Config {
NODE_ENV: string;
PORT: string;

MESSENGER_API_ENDPOINT: string;
GENERAL_RUNTIME_ENDPOINT: string;
VERIFY_TOKEN: string;

Expand Down

0 comments on commit db925c7

Please sign in to comment.