Utility functions to communicate with BERNARD.
For simplicity reasons, the source file can be loaded as-is in a browser or in webpack.
yarn add bernard-js
And then
import bernard from 'bernard-js';
Add index.js
to your project and load it as a script.
Three authentication mecanisms are provided out of the box, namely:
-
urlTokenAuth
looks at a_b
parameter in the query string -
hashTokenAuth
uses the hash to find the token and removes it once consumed, which makes it more secure thanurlTokenAuth()
. -
messengerExtensionsAuth
uses messenger extensions
You can provide as many authentication methods as you want.
bernard.getUser(
[
bernard.hashTokenAuth(),
bernard.urlTokenAuth(),
bernard.messengerExtensionsAuth(FB_APP_ID)
],
(err, user, token) => {
if (err) {
return console.log('Could not authenticate!');
}
// Sends a message with a Postback layer to the bot
bernard.sendPostback(token, {action: 'foo'});
// Tracks the current page into the analytics providers
bernard.pageView(token);
}
);
Of course, don't forget to replace FB_APP_ID
with your own Facebook
app ID.
Let's break it down.
- You create a list of authentication mecanisms you want to try
[
bernard.hashTokenAuth(),
bernard.urlTokenAuth(),
bernard.messengerExtensionsAuth(FB_APP_ID)
],
- You call
getUser()
bernard.getUser(
[
bernard.hashTokenAuth(),
bernard.urlTokenAuth(),
bernard.messengerExtensionsAuth(FB_APP_ID)
],
(err, user, token) => {
// ...
}
);
- You save the
token
and maybeuser
info. You will need thetoken
for any other communication with the server.
// Sends a message with a Postback layer to the bot
bernard.sendPostback(token, {action: 'foo'});
// Tracks the current page into the analytics providers
bernard.pageView(token);
This is a very important point
bernard.js
will make HTTP requests to your BERNARD instance. Since
BERNARD doesn't serve your front-end, it means that BERNARD and your
front-end will run on different domains. Let's say:
-
bot.foobar.com
is your BERNARD instance -
front.foobar.com
is your front-end instance
You have several options here, but by far the most simple is to create
a proxy that maps front.foobar.com/postback
to
bot.foobar.com/postback
. Otherwise you'd have to deal with CORS and
other security-related problems.
Example nginx configuration for this:
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
/* All sorts of things go here, like SSL configuration */
index index.html;
server_name front.foobar.com;
location / {
try_files $uri $uri/ =404;
gzip_static on;
}
location /postback {
proxy_pass https://bot.foobar.com;
}
}
Simply doing so will allow your front-end to seamlessly communicate with your bot.
You might have noticed that there is two authentication methods.
If your bot runs in Messenger, you should use the Messenger Extensions as they are way more secure than using a token in the URL (since the user could just copy/paste the URL to a friend by trying to share the page).
In order for the messenger extensions, you must:
- Add the domain of your front-end to the page's whitelist. By example,
set add to the bot's environment variables
FRONT_BASE_URL=https://front.foobar.com
and then put in your bot'ssettings.py
(provided that you use the default template):
def make_whitelist():
out = []
extract_domain('BERNARD_BASE_URL', out)
extract_domain('FRONT_BASE_URL', out)
return out
# ...
PLATFORMS = [
{
'class': 'bernard.platforms.facebook.platform.Facebook',
'settings': [
{
# ...
'whitelist': make_whitelist()
},
],
}
]
- When you make a URL Button, always set
messenger_extensions
toTrue
. Example state from a hypotheticalstates.py
file:
class TestState(MyBaseState):
async def handle(self) -> None:
url = 'https://front.foobar.com/'
self.send(
fbl.ButtonTemplate(
text='Foo',
buttons=[
fbh.UrlButton(
title='Bar',
url=url,
messenger_extensions=True,
),
]
)
)
In the case where you're using the hash token, all you need to do is to
sign the URL with the self.request.sign_url()
function. It is pretty
similar to urlTokenAuth()
but is a little bit more secure since the
token is removed from the URL once loaded.
By example:
class TestState(MyBaseState):
async def handle(self) -> None:
url = await self.request.sign_url('https://front.foobar.com/')
self.send(
fbl.ButtonTemplate(
text='Foo',
buttons=[
fbh.UrlButton(
title='Bar',
url=url,
),
]
)
)
In the case where you're using the URL token, all you need to do is to
sign the URL with the self.request.sign_url()
function. It is a bit
less secure than hashTokenAuth()
so please avoid using it if you don't
need to.
By example:
class TestState(MyBaseState):
async def handle(self) -> None:
url = await self.request.sign_url(
'https://front.foobar.com/', method=self.request.QUERY
)
self.send(
fbl.ButtonTemplate(
text='Foo',
buttons=[
fbh.UrlButton(
title='Bar',
url=url,
),
]
)
)
/**
* Authenticate by URL token. The `tokenName` parameter is optional,
* default value is _b.
*/
function urlTokenAuth(tokenName) {
// ...
}
/**
* Authenticates using the hash fragment. It is more secure than the
* url token because it will automatically be removed from the URL once
* consumed.
*/
function hashTokenAuth() {
// ...
}
/**
* Authenticate the user using Messenger Extensions
* @param appId {String} Facebook app ID
*/
function messengerExtensionsAuth(appId) {
// ...
}
/**
* Install the messenger extensions, wait for them to load and calls the
* callback with the extensions as parameter.
*/
function messengerExtensionsReady(cb) {
// ...
}
/**
* Authenticate and get the user.
*
* @param methods {Function[]} Authentication functions to be called
* @param cb {Function} Node-style callback (cb(err, user)) that will be
* called when there's a user.
* @param endpoint {String} Optional parameter. Address of the endpoint to
* use for authentication
* @param tokenKey {String} Key of the token to use
*/
function getUser(methods, cb, endpoint, tokenKey) {
// ...
}
/**
* Sends a postback message to the user
*
* @param token {String} Token returned by `getUser()`
* @param payload {Object} Payload to be sent
* @param cb {Function} Callable that will receive the result. First
* argument will be the error if any, undefined
* otherwise.
* @param endpoint {String} Optional endpoint URL
* @param tokenKey {String} Optional custom name for custom token key
*/
function sendPostback(token, payload, cb, endpoint, tokenKey) {
// ...
}
/**
* Track a page view in the analytics provider(s)
*
* @param token {String} Token returned by `getUser()`
* @param path {String} URL path. By default, uses the current path.
* @param title {String} Page title. By default, uses the current title.
* @param cb {String} Optional callback called when the tracking is done.
* @param endpoint {String} Optional custom endpoint URL
* @param tokenKey {String} Optional custom token key
*/
function pageView(token, path, title, cb, endpoint, tokenKey) {
// ...
}