npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

comet-messenger

v1.2.2

Published

Yet another way to interact with Facebook Messenger in Node-JS

Downloads

12

Readme

comet-messenger

NPM Circle CI Coverage Status

Node-JS microservice-oriented framework for interacting with Facebook Messenger.


Some important factors to bear in mind:

  • Comet makes absolutely no assumptions about your architecture, but does assume the API service will be attached to an Express application (happy to accept PRs for more web frameworks).
  • It does provide some structure for building your own messenger bots.
  • async/await functions are used in the core, and are encouraged.
  • Follows the hop naming convention: :beers:.

Installation

$ npm install comet-messenger

If preferred you can clone directly from the GitHub repository. master is guaranteed to be stable, but there's no guarantee of changes between major versions breaking existing code. Trust Semver.

Usage

In Comet, the concept of a bot is split into pages, schema, router and worker.

pages exist as an array of objects defining a page. At a minimum you will need the page ID, the name of the page, and the page token you created on Facebook.

[
  {
    "id": "24733546793321423..",
    "name": "Some Important Page",
    "token": "f03fc501fca315b369b00fc0077b5cf7.."
  }
]

Think of the schema as a conversation tree - every outcome is mapped and this is how you programmatically control your Messenger bot. All your business logic is going to end up in here! See the Schema API section for more information on the methods here and other methods you can use.

const comet = require('comet-messenger');
const schema = comet.createSchema();

/**
 * In Messenger, a postback occurs when a user clicks a button.
 */
schema.onPostback('GETTING_STARTED', function ({ payload, send }) {
  return send([
    `Hey there!`,
    {
      type: 'text',
      text: text('HIGH_FIVE_REQUEST', 'How about a high-five?', { user }),
      messenger_buttons: [
        {
          type: 'postback',
          title: '✋',
          payload: JSON.stringify('RECEIVE_HIGH_FIVE'),
        },
        {
          type: 'postback',
          title: 'Nah',
          payload: JSON.stringify('REJECTED_HIGH_FIVE'),
        },
      ],
    },
  ]);
});

/**
 * Once a state has been set, free-form input can be accurately handled!
 */
schema.catchInput(function ({ send }) {
  return send([
    'Hmm, I didn\'t quite understand that',
    {
      type: 'text',
      text: 'How about that high-five?',
      messenger_buttons: [
        {
          type: 'postback',
          title: '✋',
          payload: JSON.stringify('RECEIVE_HIGH_FIVE'),
        },
        {
          type: 'postback',
          title: 'Nah',
          payload: JSON.stringify('REJECTED_HIGH_FIVE'),
        },
      ],
    },
  ]);
});

The router is the API router that the Facebook webhook will hit. It takes messages sent from Facebook and pushes the messages onto your preferred queue stack. This example assumes you'll be dropping it into an existing Express application and using async for your queue. The router requires your Facebook app secret & the verify token that you specified when setting up that Facebook webhook.

const async = require('async');
const comet = require('comet-messenger');
const pages = [ ... ]; // The array of pages from above, for example

const queue = async.queue((payload, callback) => { ... });

app.use('/api/fb-messenger-bots', comet.createExpressRouter({
  /**
   * Your list of Facebook pages
   */
  pages,

  /**
   * A function which accepts a Promise in return, so you can push messages onto your preferred queue
   * stack. Since this function is wrapped around an await call, it doesn't matter if this function is
   * asynchronous or not.
   *
   * @param Array payloads
   * @return Promise
   */
  queue: payloads => queue.push(payloads),

  // app_id: 6722778727758416..,
  app_secret: 'd3163a6893132fd0ccdffa1bb7cfee82..',
  verify_token: 'some-random-string-of-your-choice',

  // logger: { ... } Any instance/object that has info, warn & error methods
}));

Finally, the worker operates at the other end of your preferred queue stack, processing messages and sending replies on behalf of your bot.

const async = require('async');
const comet = require('comet-messenger');
const pages = [ ... ]; // The array of pages from above, for example

const schema = comet.createSchema();
/* Omitted here is all the business logic & methods used to configure the schema */

const worker = comet.createWorker({ pages, schema });
const queue = async.queue((payload, callback) => {
  worker.process(payload).then(() => callback()).catch(err => callback(err));
});

Schema API

The schema holds all the business logic of your bot, and has several methods to help you map out your conversation as you see fit.

The Messenger platform sends hooks to your API for you to consume (and hopefully pass to Comet!). But their main form of communication is a little.. fractured. They send messages and they send postbacks, which are supposed to be when a user presses a button, but they don't specify how this should be properly used (and if a user scrolls back through the conversation they can re-fire a postback, which plays havoc with any sort of state management!).

So, in order to continue this separation of concerns, Comet lets you setup functions for raw input and postbacks.

schema.onPostback(pointer, function)

This sets a function for a particular postback type, and will execute that function when this postback is received.

Important note: Comet does a little pre-processing on these postbacks, most importantly taking a string and transforming it into { type: "%s" }. Why? This immediately allows for complex postback functions, with types and parameters, as long as you remember to JSON-encode any postback payloads (as seen above in the messenger_buttons array).

schema.onPostback('GETTING_STARTED', function ({ payload, send }) {
  return send([
    `Hey there!`,
    {
      type: 'text',
      text: text('HIGH_FIVE_REQUEST', 'How about a high-five?', { user }),
      messenger_buttons: [
        {
          type: 'postback',
          title: '✋',
          payload: JSON.stringify('RECEIVE_HIGH_FIVE'),
        },
        {
          type: 'postback',
          title: 'Nah',
          payload: JSON.stringify('REJECTED_HIGH_FIVE'),
        },
      ],
    },
  ]);
});

schema.catchInput(function)

This sets a function that will execute whenever free-flowing text is received. This includes attachments.

schema.catchInput(function ({ send }) {
  return send([
    'Hmm, I didn\'t quite understand that 🙁',
    {
      type: 'text',
      text: 'How about that high-five?',
      messenger_buttons: [
        {
          type: 'postback',
          title: '✋',
          payload: JSON.stringify('RECEIVE_HIGH_FIVE'),
        },
        {
          type: 'postback',
          title: 'Nah',
          payload: JSON.stringify('REJECTED_HIGH_FIVE'),
        },
      ],
    },
  ]);
});

schema.before(function)

This queues a function to run before the main input/postback function, allowing data to be fetched before all business logic. Use this to fetch the user's profile from Facebook or load state or record analytics.

schema.before(async function (req) {
  const { page, payload } = req;

  req.user = await getUserFromFacebookAndCache({ user_id: payload.user_id });
  req.state = await getStateForUser({ page: page.id, user_id: payload.user_id });
  req.pointer = req.state.getPointer(); // This could return a string, like 'SENDING_FAV_COLOR'
});

schema.after(function)

This queues a function to run after the main input/postback function.

schema.before(async function (req) {
  const { page, payload, state } = req;
  if (state.hasModified()) await saveStateForUser({ page: page.id, state, user_id: payload.user_id })
});

schema.onInput(pointer, callback)

Suddenly, if the concept of state is introduced we don't need one function to handle all free-flowing text that a user sends to a bot. Which could be a lot, they're inside a messaging app, it's sort of a given? In any case, setting req.pointer in a before function will mean Comet will look for a input function referring to a particular state, exactly like a postback function, which means we can handle all kinds of input from the user with little difficulty:

schema.onInput('SENDING_FAV_COLOR', function ({ payload, send }) {
  // If the user triggers this function, it means we KNOW we've marked them as SENDING_FAV_COLOR, so we
  // know what we're expecting. Hooray!
  const colour = (payload.text || '').trim();
  if (colour.indexOf('#') === 0) return send('Ooh, a hex code? You\'re not a developer, are you?');
  else return send(`Cool, ${colour} is my favourite colour too!`);
});

schema.onInput('SENDING_NEW_PROFILE_PIC', function ({ payload, send }) {
  const attachment = Array.isArray(payload.attachments) ? payload.attachments.shift() : null;
  if (!attachment || !attachment.type) return send('Erm, you need to attach some content');
  else return send(`Thanks for sending that ${attachment.type} through!`);
});

TODO

  • More documentation
  • Unit tests. Although this has been built in a modular-fashion unit-tests are still required! Going for 100% code-coverage too, so watch this space!

Questions