Facebookの投稿をSlackに通知するBOTを作成しよう

お久しぶりです。本間です。

本日は、エンジニアブログもお世話になっているFacebookアカウント「Adways Engineers Diary」の投稿をSlackに通知するBOTを作成したので記事にしたいと思います。

BOTの処理の流れ

  1. UserAccessTokenの取得
  2. Adways Engineers Diary の投稿(フィード)の取得
  3. Slackに通知

事前準備

  • node.js v4.1.2
  • PhantomJS 2.1.1
  • Facebook開発者アカウント
  • Facebookアプリ作成

Facebookアプリの作成

Adways Engineers Diary のフィードの取得するためにFacebookアプリを作成します。
(※Facebook開発者アカウントは事前に登録しておいてください)

Facebookにログインするとサイドバーメニューの下の方に「開発者 => アプリを管理」のリンクがあるのでそこから作成します。
img1

リンクをクリックすると自分のFacebookアプリ一覧が表示されます。
右上の「+新しいアプリを追加」から新しいアプリを追加します。
img2

アプリの種類を聞かれるのでウェブサイトを選びます。

「Skip and Create App ID」でササっとアプリを作ります。
img3

アプリの名前とメールアドレス、カテゴリを入力して「アプリIDを作成」を押して作成します。

作成が完了するとダッシュボードに移動します。UserAccessTokenを取得するためにログインが必要なので、 「Facebookログイン => スタート」を選択します。
img4

認証後にリダイレクトするURIを設定します。IPアドレスBOTを動かすPCのローカルIPアドレスを設定します。
ポートは適当でもいいですが後で必要なので覚えておいてください。
img5

最後にBOTからこのアプリを使うためにアプリIDとapp secretをメモってください。
以上でFacebookアプリの作成は完了です。

BOTの作成

  1. UserAccessTokenの取得
  2. Adways Engineers Diary の投稿(フィード)の取得
  3. Slackに通知

1. UserAccessTokenの取得

Adways Engineers Diaryの投稿を取得するためにFacebook Graph APIを使います。
Facebook Graph APIを使うためにはOAuth認証を行ってUserAccessTokenを取得する必要があります。
OAuth認証を一から実装するのは大変そうなのFacebookの認証もサポートしているNode.jsのモジュールpassport-facebookを使います。
BOTが認証を行ってUserAccessTokenを取得しやすいように認証後のレスポンスでUserAccessTokenを返すようにExampleを変更します。

レスポンスを返す時にAccessTokenを参照できるようにSessionにセットしておきます。
ついでにアプリID, App Secret, コールバックURLをconfigファイルで管理できるようにしておきます。

var express  = require('express');
var passport = require('passport');
var Strategy = require('passport-facebook').Strategy;

var fs     = require('fs');
var config = JSON.parse(fs.readFileSync('./config.json', 'utf8'));

passport.use(new Strategy({
    clientID: config.clientID,
    clientSecret: config.clientSecret,
    callbackURL: config.callbackURL
  },
  function(accessToken, refreshToken, profile, cb) {
    passport.session.accessToken = accessToken;
    return cb(null, profile);
  }));

レスポンスにAccessTokenだけを返すように変更します。
ついでに、リッスンポートをconfigファイルで管理できるようにしておきます。
※リッスンポートは先ほどFacebookアプリの認証後のリダイレクトURIに設定したものと同じものを設定してください。

app.get('/',
  passport.authenticate('facebook'));

app.get('/login/facebook/return',
  passport.authenticate('facebook', { failureRedirect: '/login' }),
  function(req, res) {
    res.send(passport.session.accessToken);
  });

app.listen(config.listenPort);

これでOAuth用のサーバが準備できたのでBOTが自動でログインしてAccessTokenを取得する部分を作ります。
自動でログインするためにスクレイピングを行います。Node.jsのスクレイピングは下記を参考にしました。

async-awaitを使って非同期処理を同期処理のように書けるように作成しました。

  login() {
    return this.client
    .url(this.config.loginURL)
    .setValue('input[name=email]', this.config.email)
    .setValue('input[name=pass]' , this.config.password)
    .click('#loginbutton');
  }

  async requestAccessToken() {
    try {
      return this.accessToken =
        await this.login()
        .getHTML('body', false);
    } finally {
      this.client.end();
    }
  }

参考サイトでは「docker-selenium」を使っていましたが最小構成にしたかったので、PhantomJSとselenium-standaloneを追加いました。また、selenium-stanaloneをバックグラウンドで起動するための起動スクリプトも作成しました。

2. Adways Engineers Diary の投稿(フィード)の取得

Facebook Graph APIを使ってAdways Engineers Diary の投稿を取得する部分を作成します。
投稿を取得する時に必要な項目と期間を絞れるようにします。

期間を絞るためには日付に関する処理が必要になります。javascriptのDateオブジェクトは使いづらいので日付に関する処理はmomentというモジュールを使います。
※実際に日付に関する処理を行っているのは呼び出し側です。

Facebook Graph APIにリクエストを送るにはrequestというモジュールを使います。
そのままでも使えるのですが、bluebirdというモジュールを使ってasync-awaitを使って書けるようにしました。

リクエストURLを作成するのにはbuild-urlを使いました。

const GRAPH_API_URL = 'https://graph.facebook.com';
const API_VERSION   = 'v2.7';

 constructor(config) {
   ・・・
   // ここでasync-await対応しています。
   this.request = Promise.promisifyAll(request);
   ・・・
 }

  async requestGourmetClubFeed(since, until) {
    if ( this.accessToken === undefined )
      await requestAccessToken();

    return await this.request.getAsync({
      url: buildUrl(GRAPH_API_URL, {
        path: [API_VERSION, this.config.user_id].join('/'),
        queryParams: {
          fields: `feed.since(${since}).until(${until}){id,message}`,
          access_token: this.accessToken
        }
      }),
      json: true
    });
  }

FaceBook Graph APIのレスポンスはjsonなのでそのままだとSlackに通知できないのでjsonからSlackに通知するメッセージに変換するクラスを作成します。

const POST_URL_BASE = "https://www.facebook.com/adways.engineer/posts/";

export default class FacebookFeed {

  constructor(jsonFeed) {
    this.jsonFeed = jsonFeed;
  }

  json2message() {
    let post_url = `${POST_URL_BASE}${this.jsonFeed.id.split(/_/)[1]}`;
    let message  = this.jsonFeed.message;

    return `${post_url}\n${message}`;
  }
}

3. Slackに通知

Slack Web APIを使ってSlackに通知する部分を作成します。Slack Web APIを使うためにはTokenが必要なのでここから取得します。
img6

Slack Web APIにリクエストを送るのにslack-apiというモジュールを使います。
「Token,通知するチャンネル,通知するユーザの名前,通知するユーザのicon」をconfigファイルで管理できるようにしておきます。
Facebook Graph APIにリクエストを送るために利用したrequestモジュールはbluebirdモジュールを使ってasync-awaitに対応させましたが、slack-apiは自力でasync-awaitに対応できました。

export default class Slack {
  constructor(config) {
    this.config = config;
    // ここでasync-awaitに対応しています。
    this.slack = SlackApi.promisify();
  }

  async postMessage (text, channel=this.config.channel) {
    await this.slack.chat.postMessage({
      token: this.config.token,
      channel: channel,
      text: text,
      username: this.config.username,
      icon_url: this.config.icon_url
    });
  }
}

BOTの実行設定

先ほど作成した 1. UserAccessTokenの取得 2. Adways Engineers Diary の投稿(フィード)の取得 3. Slackに通知

を呼び出すスクリプトを作成してcronで起動するように設定します。

facebook_bot.js

async function main() {
  try {
    let config  = JSON.parse(fs.readFileSync('./config.json', 'utf8'));

    let facebookBot = new FacebookBot(config);
    let accessToken = await facebookBot.requestAccessToken();

    let now = moment();
    let since = now.clone()
            .add(-1, 'hours')
            .minutes(0)
            .seconds(0)
            .milliseconds(0)
            .unix();
    let until = now.clone()
            .minutes(0)
            .seconds(0)
            .milliseconds(0)
            .unix();
    // 1時間前から現在時刻までの投稿を取得します。
    let response = await facebookBot.requestGourmetClubFeed(since, until);

    let messages = [];
    response.body.feed.data.forEach(jsonFeed => {
      messages.push(new FacebookFeed(jsonFeed).json2message());
    });

    new Slack(config).postMessage(messages.join('\n----------\n'));
  } catch(e) {
    console.error(e);
  }
}

main();

1時間前から現在時刻までの投稿を取得するスクリプトを作成したのでcronで1時間毎に実行するように設定します。

PATH="$PATH:{nodePath}"
0 * * * * cd {downloadPath}; ./node_modules/.bin/babel-node facebook_bot.js

※ nodenvを使っている場合whichコマンドででてくるnodeのパスはスクリプトのパスなので注意
※ PATH="$PATH:~/.nodenv/versions/x.x.x/bin"

以上で作成完了です。

Facebook Graph APIここからいろいろ試せるので自分だけのBOTを作ってみてください!
img7

このBOTgithubにあるので興味がある方は見てみてください。
https://github.com/homma-masahiro/facebook_bot