Skip to content

Commit 7a185c7

Browse files
authored
Merge pull request #11 from chaosloth/conno/bot-hooks
Bot hooks
2 parents 9e41850 + defb130 commit 7a185c7

File tree

2 files changed

+266
-0
lines changed

2 files changed

+266
-0
lines changed
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { sign, decode } from "jsonwebtoken";
2+
import { Context } from "@twilio-labs/serverless-runtime-types/types";
3+
4+
/**
5+
* Sends a message to the bot
6+
* @param context - The Twilio runtime context
7+
* @param botInstanceId - The ID of the bot instance
8+
* @param body - The message body to send
9+
*/
10+
export async function sendMessageToBot(
11+
context: Context & { TWILIO_REGION?: string; ACCOUNT_SID: string; AUTH_TOKEN: string },
12+
botInstanceId: string,
13+
body: Record<string, any>
14+
) {
15+
const url = `https://ROVO_URL_HERE`;
16+
17+
const response = await fetch(url, {
18+
method: "POST",
19+
body: JSON.stringify(body),
20+
headers: {
21+
Authorization: `Basic ${Buffer.from(
22+
`${context.ACCOUNT_SID}:${context.AUTH_TOKEN}`,
23+
"utf-8"
24+
).toString("base64")}`,
25+
"Content-Type": "application/json",
26+
Accept: "application/json",
27+
},
28+
});
29+
if (response.ok) {
30+
console.log("Sent message to Bot");
31+
return;
32+
} else {
33+
throw new Error(
34+
"Failed to send request to Bot. " + (await response.text())
35+
);
36+
}
37+
}
38+
39+
/**
40+
* Reads attributes from a conversation
41+
* @param context - The Twilio runtime context
42+
* @param chatServiceSid - The Chat Service SID
43+
* @param conversationSid - The Conversation SID
44+
* @returns The parsed conversation attributes
45+
*/
46+
export async function readConversationAttributes(
47+
context: Context & { getTwilioClient: () => any },
48+
chatServiceSid: string,
49+
conversationSid: string
50+
) {
51+
try {
52+
const client = context.getTwilioClient();
53+
const data = await client.conversations.v1
54+
.services(chatServiceSid)
55+
.conversations(conversationSid)
56+
.fetch();
57+
return JSON.parse(data.attributes);
58+
} catch (err) {
59+
console.error(err);
60+
return {};
61+
}
62+
}
63+
64+
/**
65+
* Gets the bot ID from context or event
66+
* @param context - The Twilio runtime context
67+
* @param event - The event object
68+
* @returns The bot ID
69+
*/
70+
export async function getBotId(
71+
context: Context & { AUTH_TOKEN: string; BOT_ID?: string },
72+
event: {
73+
EventType?: string;
74+
botId?: string;
75+
ConversationSid?: string;
76+
ChatServiceSid?: string;
77+
}
78+
) {
79+
if (event.EventType === "onMessageAdded") {
80+
try {
81+
const { ConversationSid, ChatServiceSid } = event;
82+
const parsed = await readConversationAttributes(
83+
context,
84+
ChatServiceSid,
85+
ConversationSid
86+
);
87+
if (typeof parsed.botId === "string" && parsed.botId) {
88+
return parsed.botId;
89+
}
90+
} catch (err) {
91+
console.log("Invalid attribute structure", err);
92+
}
93+
}
94+
const botId = event.botId || context.BOT_ID || event.botId || context.BOT_ID;
95+
96+
if (!botId) {
97+
throw new Error("Missing Bot ID configuration");
98+
}
99+
100+
return botId;
101+
}
102+
103+
/**
104+
* Signs a request with JWT
105+
* @param context - The Twilio runtime context
106+
* @param event - The event object
107+
* @returns The signed JWT token
108+
*/
109+
export async function signRequest(
110+
context: Context & { AUTH_TOKEN: string },
111+
event: Record<string, any>
112+
) {
113+
const assistantSid = await getBotId(context, event);
114+
const authToken = context.AUTH_TOKEN;
115+
if (!authToken) {
116+
throw new Error("No auth token found");
117+
}
118+
return sign({ assistantSid }, authToken, { expiresIn: "5m" });
119+
}
120+
121+
/**
122+
* Verifies a request token
123+
* @param context - The Twilio runtime context
124+
* @param event - The event object containing the token
125+
* @returns Whether the token is valid
126+
*/
127+
export function verifyRequest(
128+
context: Context & { AUTH_TOKEN: string },
129+
event: { _token: string }
130+
) {
131+
const token = event._token;
132+
if (!token) {
133+
throw new Error("Missing token");
134+
}
135+
136+
const authToken = context.AUTH_TOKEN;
137+
if (!authToken) {
138+
throw new Error("No auth token found");
139+
}
140+
141+
try {
142+
// The decode function from jsonwebtoken only takes a token and options
143+
const decoded = decode(token, { json: true });
144+
if (decoded && typeof decoded === 'object' && 'assistantSid' in decoded) {
145+
return true;
146+
}
147+
} catch (err) {
148+
console.error("Failed to verify token", err);
149+
return false;
150+
}
151+
return false;
152+
}
153+
154+
// All functions are already exported using named exports
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { ServerlessFunctionSignature } from "@twilio-labs/serverless-runtime-types/types";
2+
import {
3+
signRequest,
4+
getBotId,
5+
sendMessageToBot,
6+
readConversationAttributes
7+
} from "./bot.helper.private";
8+
9+
// Import Twilio for Response object - using require for Twilio.Response compatibility
10+
const Twilio = require('twilio');
11+
12+
// Define the context interface
13+
interface BotContext {
14+
getTwilioClient: () => any;
15+
DOMAIN_NAME: string;
16+
ACCOUNT_SID: string;
17+
AUTH_TOKEN: string;
18+
[key: string]: any; // Index signature to satisfy EnvironmentVariables constraint
19+
}
20+
21+
// Define the event interface
22+
interface BotEvent {
23+
request: {
24+
cookies: Record<string, string>;
25+
headers: Record<string, string>;
26+
};
27+
Body?: string;
28+
ConversationSid: string;
29+
ChatServiceSid: string;
30+
Author: string;
31+
[key: string]: any; // Index signature for additional properties
32+
}
33+
34+
/**
35+
* Handler for Bot onMessageAdded events
36+
*/
37+
export const handler: ServerlessFunctionSignature<BotContext, BotEvent> =
38+
async function (context, event, callback) {
39+
const assistantSid = await getBotId(context, event);
40+
41+
const { ConversationSid, ChatServiceSid, Author } = event;
42+
const BotIdentity =
43+
typeof event.AssistantIdentity === "string"
44+
? event.AssistantIdentity
45+
: undefined;
46+
47+
let identity = Author.includes(":") ? Author : `user_id:${Author}`;
48+
49+
const client = context.getTwilioClient();
50+
51+
const webhooks = (
52+
await client.conversations.v1
53+
.services(ChatServiceSid)
54+
.conversations(ConversationSid)
55+
.webhooks.list()
56+
).filter((entry: { target: string }) => entry.target === "studio");
57+
58+
if (webhooks.length > 0) {
59+
// ignoring if the conversation has a studio webhook set (assuming it was handed over)
60+
return callback(null, "");
61+
}
62+
63+
const participants = await client.conversations.v1
64+
.services(ChatServiceSid)
65+
.conversations(ConversationSid)
66+
.participants.list();
67+
68+
if (participants.length > 1) {
69+
// Ignoring the conversation because there is more than one human
70+
return callback(null, "");
71+
}
72+
73+
const token = await signRequest(context, event);
74+
const params = new URLSearchParams();
75+
params.append("_token", token);
76+
if (typeof BotIdentity === "string") {
77+
params.append("_assistantIdentity", BotIdentity);
78+
}
79+
const body = {
80+
body: event.Body,
81+
identity: identity,
82+
session_id: `conversations__${ChatServiceSid}/${ConversationSid}`,
83+
// using a callback to handle AI Assistant responding
84+
webhook: `https://${
85+
context.DOMAIN_NAME
86+
}/channels/conversations/response?${params.toString()}`,
87+
};
88+
89+
const response = new Twilio.Response();
90+
response.appendHeader("content-type", "text/plain");
91+
response.setBody("");
92+
93+
const attributes = await readConversationAttributes(
94+
context,
95+
ChatServiceSid,
96+
ConversationSid
97+
);
98+
await client.conversations.v1
99+
.services(ChatServiceSid)
100+
.conversations(ConversationSid)
101+
.update({
102+
attributes: JSON.stringify({ ...attributes, assistantIsTyping: true }),
103+
});
104+
105+
try {
106+
await sendMessageToBot(context, assistantSid, body);
107+
} catch (err) {
108+
console.error(err);
109+
}
110+
111+
callback(null, response);
112+
};

0 commit comments

Comments
 (0)