Alexa + Twitter のスキルの作り方の一例

## 概要
　[こちらのAlexa Skillのハッカソンイベント](https://alexadevdaytokyo2019.splashthat.com/)に参加したときに作成したTwitter APIを使用したツイート読み上げスキルに関して、技術的な部分に興味を持たれた方が割と多かったので、どのように作ったのか記事にまとめています。

## 想定読者
 Alexa Skillを作成するにあたってNode.jsでコーディングができる方向けになります。

### そのほか必要なもの
　TwitterのAPIが使える必要があるので以下に上げるサイト等を参考にしてAPIの仕様申請を行ってください。正直言ってここが一番の難関です。
　APIが取得出来たらkeyやらtokenやらを準備します。

#### 参考
https://air1p1.com/2019/01/01/api1/
https://air1p1.com/2019/01/02/api2/

https://qiita.com/kngsym2018/items/2524d21455aac111cdee

https://www.torikun.com/entry/twitter-developer-api/

## スキルの内容
　キッズ向けのスキルで、動物のトリビアを流すものです。そのトリビアは特定のアカウントの最新ツイートを引っ張ってくるようにしました。

## スキル実装
今回楽にリクエストを送るために、NPMのtwitterのモジュールを使用しております。むろん、その他のAPIをたたくモジュールを使用してもAPIはコール可能です。

~~~js:index.js
const Alexa = require('ask-sdk-core');
const twitter = require('twitter');

const client = new twitter({
    "consumer_key"        :"twitter api の　api_key",
    "consumer_secret"     :"twitter api の　api_secret_key",
    "access_token_key"    :"twitter api の　api_token",
    "access_token_secret" :"twitter api の　api_token_secret"
})

const LaunchRequestHandler = {
    canHandle(handlerInput) {
        console.log(handlerInput.requestEnvelope.request)
        return handlerInput.requestEnvelope.request.type === 'LaunchRequest'
    },
    async handle(handlerInput) {
        // 特定のアカウントのツイートを取得してくる部分
        const params = {screen_name: 'tsurumi_moe',count:1};
        let tweets = await client.get('statuses/user_timeline', params);
        let speechText = `ちょっとうまくいかなかったから、少し時間を空けてまた聞いてね`
        let text
        if(tweets[0].text) {
            //　画像付きのツイートの場合最後にURLが入るのでそれをなくしている 
            text = tweets[0].text.split('http')[0]
            speechText = `今日のお話。\n${text}`
        }
        // 画面付きならディスプレイに表示する
        if(hasDisplay(handlerInput)){
            let directive = {
                type: 'BodyTemplate1',
                backButton: 'HIDDEN',
                title: `今日のお話`,
                token : "token",
            }
            // 画像があれば表示
            if(tweets[0].entities.media){
                const backgroundImage = new Alexa.ImageHelper()
                    .addImageInstance(tweets[0].entities.media[0].media_url_https)
                    .getImage();
                directive.backgroundImage = backgroundImage
            }
            // テキストの表示
            if(text){
                const textContent = new Alexa.RichTextContentHelper()
                    .withPrimaryText(text)
                    .getTextContent();
                directive.textContent = textContent   
            }
            handlerInput.responseBuilder.addRenderTemplateDirective(directive)
        }
        return handlerInput.responseBuilder
            .speak(speechText)
            .withShouldEndSession(true)
            .getResponse()
    }
};
// 以下省略
~~~

部分的にかいつまんで説明すると

~~~js
const client = new twitter({
    "consumer_key"        :"twitter api の　api_key",
    "consumer_secret"     :"twitter api の　api_secret_key",
    "access_token_key"    :"twitter api の　api_token",
    "access_token_secret" :"twitter api の　api_token_secret"
})
~~~

この部分でtwitter api で取得するkey等を設定してます。

~~~js
const params = {screen_name: 'tsurumi_moe',count:1};
let tweets = await client.get('statuses/user_timeline', params);
~~~

検索する対象となるアカウントを指定して、検索で取得する個数をパラメーターに設定して検索してます。(現状ハッカソンで使用していたアカウントのツイートをそのまま持ってくると問題ある可能性があったので、押しのアイドルちゃんのツイッターのアカウントがコードに書かれているのは悪しからず...)

あとは画像やテキストをコメントつけたように適切に処理してあげればアレクサが最新のツイートを読み上げてくれます。

### はまりポイント
　以下のように書くとAlexaが反応しなくなるので注意です。

~~~js
client.get('statuses/user_timeline', params,function(error, tweets, response) {
  //いろんな処理
  return handlerInput.responsBilder.getResponse()
})
~~~

理由はコールバック中にresponseがreturnされてもアレクサには正しくresponseが送られないようだからです。ほかの関数を使うときにも割とはまってしまうと思うので、コールバックではなく`async/await`でコードを書くようにしたほうがよさげです。

#### 2019年6月25日追記
せーのさん(twitter:[https://twitter.com/chao2suke](https://twitter.com/chao2suke),技術メディア:[Developers.IO](https://dev.classmethod.jp/author/seino-tsuyoshi/))からご教示いただきました。ありがとうございます。

> 要するにTwitter APIが非同期で動くから、処理が返ってくる前にlambdaのプロセスが終わってしまうのでresponseが戻らない、て事なのかと。lambdaあるある。
asyncとかpromise以外だとcontext.callbackWaitForEmptyEventLoop、とかをいじる感じかな。

つまり、こういう流れになるのかと。
1. Alexaが Lambdaを呼び出す
2. Lambdaでリクエストに該当する`LaunchRequestHandler`の`handle`内の処理が実行される
3. TiwitterのAPIを呼び出してる部分は非同期なので結果が返ってくるまでコールバックは実行待ち
4. 結果が返ってくる前に`handle`内の処理がすべて終わってしまいコールバックが実行される前にLambdaのプロセスが終わる
5. Alexa 側では正しいレスポンスが得られなかったためSessionEndRequestの処理を再度リクエスト

ログを確認するとセッションエンドリクエストには以下のようなエラー理由が含まれていました。
このようなエラーを見たら非同期等の理由でうまくレスポンスが返せてないことを疑ってみるといいかもしれません。

~~~
error: 
{ type: 'INVALID_RESPONSE',
message: 'SpeechletResponse was null' } 
~~~

## まとめ
　見ての通り今回作成したスキルは大したことやってません(汗)
　よくツイートをつぶやいたりできないのかと聞かれたんですが、それには別途アカウントリンクのシステムを導入する必要があります。それを使わない場合、APIに紐づいているアカウントからスキルを使った不特定多数の方々のツイートが垂れ流されてしまいます。(私の技術力ではそこまでなので、何かいい方法を知ってる方がいらっしゃれば教えてほしいです)
　また今回僕のチームで作成したスキルはVUIという面では開いて終わりだったので面白みがなかったですが、セッションをとっておいて何個か連続してツイートをとってこれるともう少し楽しめるものができると思いました。

## ハッカソンの感想
　ほかのチームが作っていたスキル面白いものが多くてすごく学べました。今日見たスキルが今後リリースされるのが楽しみです。