SlackとIFTTTで高効率なソーシャル・マーケティング

企業や組織・チームなどでソーシャルネット・アカウントを運用する場合、「サイトを更新したらSNSに投稿する」など、「あれが起こったら、これをする」的なアクションを行うことがよくある。これをシンプルかつ円滑に行うためにSlackとIFTTT、node.jsでカスタムWebアプリケーションを構築したので、ここで紹介する。

ソーシャル・マーケティングで効果を上げるために解決すべき課題

以前から解決しなければならないと考えていた、ソーシャル・マーケティングにおける課題には以下のものがある。

  • 別の人がFacebookページに投稿したことを、常にリアルタイムで把握する
  • Facebookページに投稿された内容を確認した後に、その内容をモバイル・アプリケーション(以下、モバイルApp)にプッシュ通知する
  • Webサイトのニュースページが更新されたことを、常にリアルタイムで把握する
  • ニュースの内容を確認した後に、その内容をTwitterにポストする

「Facebookに記事を投稿したら、モバイルAppにプッシュ通知する」とか「サイトを更新したらSNSに投稿する」など、「あれが起こったら、これをする」的アクションは、活用するネットサービスが多岐にわたるほど一元管理が難しくなる。しかも、複数人で一つのソーシャルネット・アカウントを共同運用していると、他のスタッフが投稿したものをタイムリーに把握して、素早く次のアクションに繋げることが大事。これが後手後手になってしまったり疎かになると、ソーシャル・マーケティングが効果を発揮しにくくなってしまう

それを今回、以下の3つの仕組みを使って解決することに成功した。

  • IFTTT
  • Slack
  • node.js

「インターネットのセンサー」であるIFTTTとSlackを組み合わせる

「あれが起こったら、これをする」を実現するネットサービスとして有名なものにIFTTT(イフトと発音するらしい)がある。“If This Then That”というシンプルな文脈の中、ThisとThatにさまざまなWebサービスを当てはめることで、それらを連携できるというもの。

IFTTTが提供する「これ」と「あれ」の種類は本当に多彩で、「Facebookページに投稿したら、Twitterにポストする」みたいな誰でも思いつくものから、「東京で雨が降ったら、Googleカレンダーに記録する」といった普通だと結びつかない2つを繋げて、自動化サービスを実現してくれる優れものだ。

IFTTTは「あれとこれを繋げたい」という明確なニーズが自分の中になければ、なかなか触手が伸びない類いのサービスだが、最近ではスマートスピーカーやIoT機器までを網羅しているので、ニーズが思いつけばあの手この手を使って大抵は実現できてしまう。自分はIFTTTを「インターネットのセンシング(センサー)・サービス」と捉えているが、この辺りの話題はまた別の記事で。

このようにIFTTTを使えばFacebookやTwitterの投稿を把握できるし、RSSが更新されたことも把握できる、それをトリガーにして各種SNSにポストすることもできるので、組み合わせればIFTTTだけで完全自動化ができる。しかし、投稿内容を確認することこそが重要で、「今日は通知を送りすぎてはいないか?」「このテキストだとニュースヘッドラインには良いがTwitterでは分かりにくいのでは? 」といった判断のための目視確認というプロセスは外せない。その上で、投稿にスターをつけるなどできるだけ簡単な操作で次のアクションに繋げたい。

色々と考えた結果、Slackを使うことが一番適していることがわかった。具体的には、SNS投稿やWeb更新をIFTTTが察知すると、その内容をSlackに自動投稿する。このタイミングで手元のSlackアプリケーションにプッシュ通知が届くので、他のスタッフによる投稿をリアルタイムに把握できる。次にSlackで投稿内容を確認し、そのまま次のアクションに渡してもOKならSlack上でスターをつける。一方、次のアクションに渡す際にテキストを変更したい場合は、Slackに変更したテキストを投稿するという仕組みだ。

node.jsで課題解決のためのカスタムWebアプリケーションを構築

Slackの特長はその豊富なAPI群。そのうちの一つであるEvents APIというのを使うと、メッセージが投稿されたりスターが付くとプログラム処理を実行できる。今回はスターが付いた場合か、冒頭に #push というタグが書かれたメッセージが投稿されたら、NiftyCloud mobile backendを経由してモバイルAppにプッシュ通知を配信するようにしたい。

Webサイトのニュース記事をTwitterにポストするのも同様で、スターを付けるか、冒頭に #tweet と書かれたメッセージを投稿すると、その内容をIFTTTに戻してTwitterへポストするようにしたい。

IFTTT、Slack、NiftyCloud mobile backendと利用するサービスが増えてきたが、これらが一様にモジュールを提供しているプログラム開発・実行環境にnode.jsがある。node.jsはサーバーサイドでJavaScriptを実行できるもので、使い慣れたJavaScriptでPHPやJavaのようなサーバーサイドプログラムが書けるということから、人気が高いし敷居は低い。

幸いにもWonderwallのサーバーでは以前からnode.jsを動かしていたので、この上でプログラムを自作することにした。

実現にあたっての作業とソースコードの内容

プログラムの詳細については実際のソースコードを記載するのでそちらを見てもらうとして、作業全体の流れは以下のようになっている。

・事前準備

  1. Slackに専用のチャンネルを設置
  2. Slackチャンネルにボットを常駐させる
  3. Slack APIで動作するSlack Appを追加。
  4. イベントを有効化。その際に設定するリクエストURLは、node.jsで構築したWebアプリケーションのURLにする
  5. イベントに message.channels と star_added を設定する
  6. IFTTTで、Facebookページに投稿されたらSlackチャンネルに投稿するアプレットを用意
  7. IFTTTのIf Thisにwebhook(指定されたURLが叩かれたことを検知する仕組み)を指定し、Then Thatにはwebhookが叩かれたらTwitterにツイートするというアプレットを用意

・node.js Webアプリケーションのロジック

  1. node.js上でHTTPSサーバーを起動する。そのためのSSL証明書と秘密鍵を読み込む。
  2. Slack Events APIからイベントを取得するためのKeyを設定する。
  3. Slack Events APIでメッセージをキャッチしたら冒頭のハッシュタグを調べて、#push ならモバイルAppへプッシュ通知、#tweet ならTwitterへ投稿する処理を行う。
  4. Slack Events APIでスターが付いたことをキャッチしたら、その投稿内容の一行目を取得。Facebookページの投稿にスターが付いたならモバイルAppへプッシュ通知、ツイートならTwitterへ投稿する処理を行う。
  5. Slack上の操作に応じてボットが発言するインタラクションも用意する。

…と、大体はこんな感じである。

自分たちのニーズに合致する形で複数のサービスを連携させるには、プログラム的発想と、さまざまなWebサービスの特性を把握しておく必要があるので敷居は高いが、課題が明確であれば必ず解決手段があるのがインターネットの世界なので、その事だけでも皆さんの頭の隅に置いておいてもらえると幸いです。

参考資料:利用したnode.jsのモジュール一覧と作成したソースコード全文

このWebアプリケーションを実現するにあたり、node.jsに以下のモジュールを追加した。

Express:
Node.js Web アプリケーション統合フレームワーク。node.js上でのhttp/httpsサーバーの実行からミドルウェア的な働きまで行う
http://expressjs.com/ja/

request:
HTTP/HTTPS 通信を行うためのクライアント・モジュール
https://github.com/request/request

body-parser:
Expressで受け取ったPOSTデータをJSONにする
https://www.npmjs.com/package/body-parser

Slack Developer Kit for Node.js:
今回はEventsAPIを処理するために利用
https://github.com/slackapi/node-slack-sdk

NiftyCloud mobile backendのnode.jsモジュール:
モバイルAppにプッシュ通知を配信するサービスとしてmobile backendを利用しているが、NiftyCloudはnode.jsで簡単に利用できるモジュールを提供している http://mb.cloud.nifty.com/doc/current/introduction/quickstart_javascript.html

forever:
node.jsアプリケーションを実行する際の死活管理を行うモジュール
https://github.com/foreverjs/forever

var https = require('https');
var fs = require('fs');
var options = {
    key: fs.readFileSync('/Users/<strong><em>/Documents/node-env/</em></strong>.wonderwall.net.pem','utf8'),
    cert: fs.readFileSync('/Users/<strong><em>/Documents/node-env/</em></strong>.wonderwall.net.crt','utf8'),
};

var postType = {
        facebook : 1,
        twitter : 2,
        push : 3,
        tweet : 4
    };

// Initialize using verification token from environment variables
const createSlackEventAdapter = require('@slack/events-api').createSlackEventAdapter;
const slackEvents = createSlackEventAdapter('**SLACK_VERIFICATION_TOKEN**');
const port = process.env.PORT || 3000;

// Initialize an Express application
const express = require('express');
const bodyParser = require('body-parser');
const app = express();

// You must use a body parser for JSON before mounting the adapter
app.use(bodyParser.json());

// Mount the event handler on a route
// NOTE: you must mount to a path that matches the Request URL that was configured earlier
app.use('/slack/events', slackEvents.expressMiddleware());

// Attach listeners to events by Slack Event "type". See: https://api.slack.com/events/message.im
slackEvents.on('message', (event)=> {
    var dict = JSON.stringify(event);
    console.log(`Receiced Dictionary: ${dict}`);

    if (event.user == "**User-ID**") { // activated user only
        if (event.text.slice(0,5) == "#push") {
            checkMessageText(event.text, postType.push);
        } else if (event.text.slice(0,6) == "#tweet") {
            checkMessageText(event.text, postType.tweet);
        }   
    }
    if (event.bot_id != "**BOT-ID**" && event.user != "**User-ID**") {
        var message = "メッセージの先頭に #push を付けるとDaisyHeadsに通知、 #tweet を付けるとMWSがツイートします。また、FB投稿に&#x2b50;&#xfe0f;を付けると先頭の一行が通知、ツイートに&#x2b50;&#xfe0f;を付けると全文がMWSでツイートされます。";
        theBotPostsToSlack(message);
    }
});

slackEvents.on('star_added', (event)=> {
    var dict = JSON.stringify(event);
    console.log(`Receiced Dictionary: ${dict}`);

    if (!event.item.message.attachments) { // IFTTTから送信された形式にマッチしなければ終了
        return;
    }

    var username = event.item.message.username; 
    var message = event.item.message.attachments[0].text;
    var title_link = event.item.message.attachments[0].title_link;

    if (username == "IFTTT") {
        if (title_link.match(/twitter.com/)) {
            checkMessageText(message, postType.twitter);
        } else if (title_link.match(/facebook.com/)) {
            checkMessageText(message, postType.facebook);
        }   
    }
});

// Handle errors (see `errorCodes` export)
slackEvents.on('error', console.error);

// Start the express application
https.createServer(options,app).listen(port, '***.wonderwall.net');

function checkMessageText(message,type) {
    if (!message) {
        return;
    }

    var result = "";

    message = message.replace(/\r\n|\r|\n/,' ')
        .replace(/</g, '')
        .replace(/>/g, '');

    if (type == postType.push) {        
        console.log("This message contains a word: #push");
        result = message.substring(5); // "#push "
        if (result) {
            sendPush(result);
        }
    } else if (type == postType.tweet) {
        console.log("This message contains a word #tweet");
        result = message.substring(7); // "#tweet "
        if (result) {
            sendTweet(result);
        }
    } else if (type == postType.facebook) {
        result = getFirstLine(message);
        if (result) {
            sendPush(result);
        }
    } else if (type == postType.twitter) {
        sendTweet(message);
    } else {
        return;
    }
}

function getFirstLine(message) {
    var ary = message.split(/\r\n|\r|\n/);
    return ary[0];
}

function sendPush(message) {
    console.log(`Push message is ... ${message}`);

    var NCMB = require("ncmb");

    var ncmb_standard = new NCMB("**API-Key**","**Client-Key**"); 

    var ncmb = ncmb_standard;
    var date = new Date();
    date.setMinutes(date.getMinutes()+10); // 現在時刻より10分後

    var push = new ncmb.Push();

    push.set("immediateDeliveryFlag", false)
        .set("title","DaisyHeads News")
        .set("sound", "")
        .set("deliveryTime",date)
        .set("message", message)
        .set("target", ["ios"])

    push.send()
        .then(function(push){
            var bot_message = date + "に送信するPush通知をセットしました。";
            theBotPostsToSlack(bot_message);
        })
        .catch(function(err){
            var bot_message = "Push通知のセット処理でエラーが発生しました:" + err;
            theBotPostsToSlack(bot_message);
        });
}

function sendTweet(message) {

    var IFTTT_event = "post_tweet";
    var key = '**IFTTT-Key**';

    var webhookUrl = `https://maker.ifttt.com/trigger/${IFTTT_event}/with/key/${key}`;

    var request = require('request');

    var options = {
        uri : webhookUrl,
        form : {value1:message,value2:"",value3:""},
        json : true
    };
    console.log('sendTweet options:'+JSON.stringify(options));

    request.post(options, function(error, response, body){
        if (!error && response.statusCode == 200) {
            var reaction = JSON.stringify(body);
            theBotPostsToSlack(reaction);
        } else {
            var reaction = 'Oops! An error has occurred: '+response.statusCode; 
            theBotPostsToSlack(reaction);
        }
    }); 
}

function theBotPostsToSlack(message) {
    var request = require('request');
    request.post('https://slack.com/api/chat.postMessage', {
        form: {
            token: '**TOKEN**',
            channel: 'sns-post',
            username: 'BOT',
            text: message
        }
    }, (error, response, body) => {
        console.log('theBotPostsToSlack error:' + error);
    });
}