Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Twitter integration #1

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 228 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,228 @@
# custom-channel-middleware
Middleware for custom channels to feed into Twilio Flex
# Customer Channel Middleware

Middleware for custom channels to feed into [Twilio Flex](https://www.twilio.com/flex)

## Twitter

The following guide demonstrates how you can enable Twilio Flex to communicate via Twitter DMs.

### Setup a Twilio Flex instance and middleware

Follow [this guide](https://www.twilio.com/blog/add-custom-chat-channel-twilio-flex) to setup your Flex instance, including the optional portions.

### Obtain a Twitter developer account

You will need access to the Twitter API, which you can apply for [here](https://developer.twitter.com/en/apply-for-access).

Setup an App and update the permissions to `Read, Write, and Direct Messages`. Note that you will need to regenerate your credentials when you modify this setting.

Create a developer environment [here](https://developer.twitter.com/en/account/environments) for the `Account Activity API` and name it `dev`.

### Install autohook

Once you have your Twitter developer account, you can set up the environment variables as described [here](https://github.com/twitterdev/autohook#dotenv-envtwitter). Installation instructions can be found [here](https://github.com/twitterdev/autohook#install).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: what's really needed later is to add twitter-autohook to your package.json in the middleware project further down.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we simply specify that you will need to run npm install twitter-autohook?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An important note on the env variables is that NGROK_AUTH_TOKEN is not actually optional. You need to create a (free) ngrok account for this to work.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch!


### Setup Twilio serverless function

The Twilio serverless function will be responsible for responding to Twitter DMs via Twilio Flex.

After you create the function, replace the code with the following:

```javascript
exports.handler = function(context, event, callback) {
if (event['Source'] == 'SDK') {
let Twit = require('twit')
let T = new Twit({
consumer_key: process.env.TWITTER_CONSUMER_KEY,
consumer_secret: process.env.TWITTER_CONSUMER_SECRET,
access_token: process.env.TWITTER_ACCESS_TOKEN,
access_token_secret: process.env.TWITTER_ACCESS_TOKEN_SECRET,
timeout_ms: 60*1000, // optional HTTP request timeout to apply to all requests.
strictSSL: true, // optional - requires SSL certificates to be valid.
})
let sendTo = {
event: {
type: 'message_create',
message_create: {
target: {
recipient_id: event['recipient_id'],
},
message_data: {
text: event['Body'],
},
},
}
}
T.post("direct_messages/events/new", sendTo, function(err, data, response){
console.info(data);
});
};
}
```

In the Settings section, add the following dependency: `twit` with version `2.2.11`.

In the Settings section, add the following environment variables: `TWITTER_ACCESS_TOKEN_SECRET`, `TWITTER_ACCESS_TOKEN`, `TWITTER_CONSUMER_SECRET`, `TWITTER_CONSUMER_KEY`

Now, save your function and click `Deploy All` and copy the URL for your function, you will need it to update the middleware.

### Update middleware

In `./twilio-flex-custom-webchat/middleware/server.js`, replace all the code there with:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: this code can be downloaded from https://github.com/vernig/twilio-flex-custom-webchat/

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a link to that repo in the references section.


```javascript
require('dotenv').load();

const flex = require('./flex-custom-webchat');

const { Autohook } = require('twitter-autohook');
(async ƛ => {
const webhook = new Autohook();
// Removes existing webhooks
await webhook.removeWebhooks();
// Listen for incoming direct messages
webhook.on('event', async event => {
if (event.direct_message_events) {
let active_user = event.for_user_id;
let sender_id = event.direct_message_events[0].message_create.sender_id
if(sender_id != active_user){
console.log("New message from: " + event.users[sender_id].name)
console.log(event.direct_message_events[0].message_create.message_data.text)
await flex.sendMessageToFlex(event.direct_message_events[0].message_create.message_data.text, event.users[sender_id].name, event.users[sender_id].screen_name, event['for_user_id']);
}
}
});
// Starts a server and adds a new webhook
await webhook.start();
// Subscribes to a user's activity
await webhook.subscribe({oauth_token: process.env.TWITTER_ACCESS_TOKEN, oauth_token_secret: process.env.TWITTER_ACCESS_TOKEN_SECRET});
})();
```

In `./twilio-flex-custom-webchat/middleware/flex-custom-webchat.js`, replace the code with:

```javascript
require('dotenv').config();
const fetch = require('node-fetch');
const { URLSearchParams } = require('url');
var base64 = require('base-64');

const client = require('twilio')(
process.env.TWILIO_ACCOUNT_SID,
process.env.TWILIO_AUTH_TOKEN
);

var flexChannelCreated;

function sendChatMessage(serviceSid, channelSid, chatUserName, recipientId, body) {
console.log('Sending new chat message');
const params = new URLSearchParams();
params.append('RecipientId', recipientId);
params.append('Body', body);
params.append('From', chatUserName);
console.log('Params:' + params)
return fetch(
`https://chat.twilio.com/v2/Services/${serviceSid}/Channels/${channelSid}/Messages`,
{
method: 'post',
body: params,
headers: {
'X-Twilio-Webhook-Enabled': 'true',
Authorization: `Basic ${base64.encode(
`${process.env.TWILIO_ACCOUNT_SID}:${process.env.TWILIO_AUTH_TOKEN}`
)}`
}
}
);
}

function createNewChannel(flexFlowSid, flexChatService, recipientId, chatUserName) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this function is being called on every new message, and these webhooks are being created on every new message. After three or four messages in the same conversation, this results in:

RestException [Error]: Too many channel webhooks
    at success (...) {
  status: 400,
  code: 50321,
  moreInfo: 'https://www.twilio.com/docs/errors/50321',
  detail: undefined
}

so we probably need to have a different mode that just creates the webhooks once.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Nice catch!

return client.flexApi.channel
.create({
flexFlowSid: flexFlowSid,
identity: recipientId,
chatUserFriendlyName: chatUserName,
chatFriendlyName: 'Flex Custom Chat',
target: chatUserName
})
.then(channel => {
console.log(`Created new channel ${channel.sid}`);
return client.chat
.services(flexChatService)
.channels(channel.sid)
.webhooks.create({
type: 'webhook',
'configuration.method': 'POST',
'configuration.url': `<Twilo Flex function URL>?channel=${channel.sid}&recipient_id=${recipientId}`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's say we have two Twitter handles... a @myHelpline handle for the helpline, and a @myCaller handle who is the person reaching out to the helpline. When @myCaller sends a DM to @myHelpline, then @myHelpline can see that DM come in via Twitter and also via Flex. Currently, when a message is sent back from Flex, it doesn't get sent to @myCaller. Instead, it gets sent to @myHelpline. Within the Twitter UI, it looks like @myHelpline is having a DM conversation with itself. We would expect that the message gets sent to the caller, though, so there is a need to adjust this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't remember that happening in the demo, it is definitely super important to test this piece of code rigorously.

'configuration.filters': ['onMessageSent']
})
.then(() => client.chat
.services(flexChatService)
.channels(channel.sid)
.webhooks.create({
type: 'webhook',
'configuration.method': 'POST',
'configuration.url': `<Twilo Flex function URL>/channel-update`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't work out if this webhook is actually needed. As it is, I don't think this does anything, since there's no function for this. Seems like this may be a hold-over from the original twilio-flex-custom-webchat code?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this is related to the issue above where we are creating multiple webhooks. Instead, we should be updating the channel.

'configuration.filters': ['onChannelUpdated']
}))
})
.then(webhook => webhook.channelSid)
.catch(error => {
console.log(error);
});
}

async function resetChannel(status) {
if (status == 'INACTIVE') {
flexChannelCreated = false;
}
}

async function sendMessageToFlex(msg, user, screen_name, sender_id) {
flexChannelCreated = await createNewChannel(
process.env.FLEX_FLOW_SID,
process.env.FLEX_CHAT_SERVICE,
sender_id,
user
);
console.log(flexChannelCreated)
console.log('DM from: ' + screen_name)
sendChatMessage(
process.env.FLEX_CHAT_SERVICE,
flexChannelCreated,
user,
sender_id,
msg
);
}

exports.sendMessageToFlex = sendMessageToFlex;
exports.resetChannel = resetChannel;
```

Replace `<Twilo Flex function URL>` with the URL obtained from your Twilio Function.

Launch the middleware in the directory `<path-to>/twilio-flex-custom-webchat/middleware` with the command `node server.js`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This actually needs several env variables to be passed for it to work:
TWILIO_ACCOUNT_SID=xx TWILIO_AUTH_TOKEN=xx FLEX_FLOW_SID=xx FLEX_CHAT_SERVICE=xx node server.js

And if you don't have ~/.env.twitter, then you need to send in the Twitter tokens and keys as well.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another nice catch! We should probably provide sample env files.


### Try it out!

[Login](https://flex.twilio.com) to your Flex instance and set your agent's status to be `Available`.

DM the Twitter account associated with the Twitter developer account and [navigate to your agent desktop](https://flex.twilio.com/agent-desktop).

Accept the message and respond.

Celebrate!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Things yet to be figured out:

  • How could we set this up with Twitter permissions such that multiple helpline handles can use it?
  • Could the middleware all be managed by Twilio functions, meaning there would be no need to maintain a separate Node app?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about point one, but I believe point two is true. I think that would make for a nice second iteration :)

#### References
* https://www.twilio.com/blog/add-custom-chat-channel-twilio-flex
* https://github.com/vernig/twilio-flex-custom-webchat
* https://github.com/twitterdev/autohook
* https://github.com/ttezel/twit

#### Contributors
* Nick Hurlburt
* Ashkon Honardoost
* Deepak Srikanth
* Elmer Thomas
* Toby Allen