Azure Functions で Twitter のリプライを受け取る WebAPI を作ってみた

BOT Framework とか使えよw というものですが、 Twitter の自動応答BOTをWebAPI化してみたかったんですね。

作りたいもののイメージ

リクエスト

https://xxxx.azurewebsites.net/api/myfunc1?text=明日の天気は?

レスポンス

晴れのちくもり

Azure Functions を作る

を参考に Javascript で作りました。 (C# で作ろうと思ったんだけど、作成直後にエラーが出たもので。そっちは MSDNフォーラムに投げたら速攻で回答してもらえた のでありがたかったけどね。)

module.exports = function (context, req) {
    // https://apps.twitter.com から得るやつ
    var oauth = {
        consumer_key: 'xxxx',
        consumer_secret: 'xxxx',
        token: 'xxxx',
        token_secret: 'xxxx'
    };

    var botTwitterName = "@hoge"; // メンション投げるTwitterアカウント名
    var botTwitterId = 999999999; // メンション投げるTwitterアカウントID
    var senderTwitterName = "@my_twitter_account "; // 自分のTwitterアカウント名
    var noComment = ['…', 'Nothing', 'EMPTY']; // リプライが得られなかった時の代替テキスト(ランダムで選ばれる)
    var waitForReply = 1000; // メンション投げてリプライを取得するまでの待ち時間

    if (!(req.query.text || (req.body && req.body.text))) {
        context.error('error! text no found');
        var res = {
            status: 200,
            body: noComment[0]
        };
        context.log("reply message:" + JSON.stringify(res));
        context.done(null, res);
        return;
    }

    var request = require('request');
    var rp = require('request-promise');

    // 会話相手との会話が以前にあったら、その続きにするために過去のツイートを取得する。
    var getLatestReplyUrl = 'https://api.twitter.com/1.1/statuses/mentions_timeline.json?count=10';
    rp({url:getLatestReplyUrl, oauth:oauth, json:true})
    .then(mentions => {
        
        // 自分の mention 群に会話相手からの投稿があったら、そのツイートIDを得る
        var latestRepId = null; 
        for (var i = 0; i < mentions.length; i++) {
            var men = mentions[i];
            if (men.user.id == botTwitterId) {
                latestRepId = men.id_str;
                break;
            }    
        }

        var formData = {"status":botTwitterName + " " + req.query.text};
        if (latestRepId != null) {
            formData["in_reply_to_status_id"] = latestRepId;
        }

        var options = {
            url: 'https://api.twitter.com/1.1/statuses/update.json',
            method: 'POST',
            headers: { 'Content-Type':'application/json' },
            json: true,
            oauth: oauth,
            include_rts: false,
            form: formData
        };
        
        return rp(options); // tweet
    })
    .then(r => {
        context.log("tweeted.res:" + JSON.stringify(r));
        return sleep(waitForReply).then(_ => r);
    }) // wait
    .then(r => { // get reply
        var getRepUrl = 'https://api.twitter.com/1.1/statuses/mentions_timeline.json?count=5&since_id=' + r.id;
        // var getRepUrl = 'https://api.twitter.com/1.1/statuses/mentions_timeline.json?count=10';
        return rp({url:getRepUrl, oauth:oauth, json:true})
        .then(mentsions => {
            context.log("get mentions." + JSON.stringify(r));
            var reply = {text:getNoComment()};
            mentsions.forEach(function(men) {
                if (men.in_reply_to_status_id == r.id) {
                    reply = men;
                }
            }, this);

            context.log("get reply." + JSON.stringify(reply));
            var res = {
                status: 200,
                body: reply.text.replace(senderTwitterName, "")
            };
            context.log("reply message:" + JSON.stringify(res));
            context.done(null, res);
        })
    })
    .catch(err => {
        context.error('error!:' + err);
        var res = {
            status: 200,
            body: noComment[0]
        };
        context.log("reply message:" + JSON.stringify(res));
        context.done(null, res);
    });

    function sleep (time) {
        return new Promise((resolve) => setTimeout(resolve, time));
    }

    function getNoComment() {
        return noComment[Math.floor( Math.random() * noComment.length )];
    }
};

やっている事

  1. 自分の過去のメンション群を取得して、相手との会話があったら、その会話の続きとするようにツイートの ID を得る
  2. 相手にメンションをツイートする(1. で ID 取れてたら in_reply_to_status_id に設定する)。投稿したツイートの ID を得ておく。
  3. n秒待つ
  4. もう一度自分の過去のメンション群を取得する。検索パラメータに投稿時のツイートIDを指定して、投稿より未来のリプライしか得ないようにする。
  5. メンション群から投稿時のツイートIDを in_reply_to_status_id に持つツイートを検索する。
  6. それがリプライのツイートなので、レスポンスでその text を返す。

使用技術

node.js でまともなプログラムを作るのは初めてでした。 次の npm モジュールにお世話になりました。

特に request を使用した Twitter API の使い方は、

を参考にさせてもらいました。

その他Tips

Azure Functions への npm install の仕方

の手順に沿うと、ブラウザ上で Terminal が使えるので、そこで npm install request-promise などとできます。package.json ?、まだよく知らないです。

ローカルでの node.js の開発環境

Mac と Visual Studio Code を使いました。

がとても参考になりました(インテリセンスを使うための設定はしませんでした)。

はじめは C# でやろうと思ったけど、こういうのは Javascript の方がちゃちゃっと作れてよいですね。お互い適材適所だなーと思いました。

(今のところトライアルだけど Azure Functions の課金ってどうなるのかな?)