TwitterでつぶやくAlexaスキルを作る

初めて投稿します。

ようやくAmazon Echoを手に入れたのでTwitterAPIを使ったAlexaスキル開発をしてみました。

本記事はプログラミングというよりも、Alexaスキル開発の流れを説明する形になっています。

#作成したスキル
今回作成したスキルは、

**Alexaに「行ってきます」「ただいま」と声をかけるとツイッターにその時間を投稿する**

というシンプルなものです。

<img width="480" alt="2018-01-18_23h01_39.png" src="https://qiita-image-store.s3.amazonaws.com/0/183744/070e504c-57c2-cac0-5b56-986b75d9a4ba.png">


<br/>
スキル名は「ツイッター」＋「ログ」で**『ツイログ』**としました。

- 「アレクサ、ツイログで行ってきます」
- 「アレクサ、ツイログでただいま」

と、~~不自然な~~発話をすることでツイートします。

ツイートが成功すると**Alexaが「つぶやきました」と喋ります。**

##注意
作成するスキルはTwitterAPIに必要なACCESS_TOKEN、ACCESS_TOKEN_SECRETをプログラム中に埋め込むため、アカウントリンクを行わない**特定のTwitterアカウント専用スキル**です。

##参考資料
Amazon公式の[Alexaスキル開発トレーニング](https://developer.amazon.com/ja/alexa-skills-kit/training/building-a-skill)で最低限必要な知識は身につきます。

スキル開発してみようと思ったら、まずはこれを全て実施しましょう。1日あれば終わります。


##全体イメージ
Alexaスキル『ツイログ』のイメージはこんな感じです。  
![無題のプレゼンテーション.png](https://qiita-image-store.s3.amazonaws.com/0/183744/7561a897-8e53-30a5-c693-82bbf160c188.png)

#Alexaスキルの開発
##TwitterApplicationManagementの登録
まず初めに外部からTwitterAPIを叩くための各種キーを取得します。（画像は過去の使い回しなので入力内容はあまり気にしないでください）

[Twitter Application Management](https://apps.twitter.com/)へアクセスし、自分のTwitterアカウントでログインします。  

次に右上の**「Create New App」**をクリックします。
<img width="400" alt="2017-12-24_00h26_32.png" src="https://qiita-image-store.s3.amazonaws.com/0/183744/8a2aaf2b-f96c-3d2d-2edf-188a51eca028.png">

登録するアプリケーションの入力フォームが出ます。**「Name」「Description」「Website」は必須入力項目**です。適当な内容で入力します。

- Name：アプリケーションの登録名
- Description：アプリケーションの説明文
- Website：アプリケーションの問い合わせ先が分かるようなURLを入力します。
- CallbackURL：今回は設定不要です。

入力したらページ最下部の**「Create your application」**をクリックします。
<img width="400" alt="2017-12-24_00h32_31.png" src="https://qiita-image-store.s3.amazonaws.com/0/183744/c302b346-431d-3c03-c0c9-96c8769a57b6.png">


これでアプリケーションの登録が完了しました。  
<img width="400" alt="2017-12-24_00h33_59.png" src="https://qiita-image-store.s3.amazonaws.com/0/183744/aee635a0-6f78-7bcc-e0fe-b655f022a197.png">


続いてAPIの利用に必要となるAccess Tokenを発行します。表示されたページのタブ**「Keys And Access Tokens」**から**「Create my access token」**をクリックします。   
<img width="400" alt="2017-12-24_00h35_06.png" src="https://qiita-image-store.s3.amazonaws.com/0/183744/b55bf6b6-6e17-551b-2fa5-d24701d97157.png">
これでTwitterAPIの準備は完了です。ページに表示されている`Consumer Key`、`Consumer Secret`、`Access Token`、`Access Token Secret`をプログラム内で利用します。  
<a href="http://f.hatena.ne.jp/WorldWorldWorld/20180119073527"><img width=400 src="http://img.f.hatena.ne.jp/images/fotolife/W/WorldWorldWorld/20180119/20180119073527.png" alt="20180119073527"></a>

<a href="http://f.hatena.ne.jp/WorldWorldWorld/20180119073742"><img src="http://img.f.hatena.ne.jp/images/fotolife/W/WorldWorldWorld/20180119/20180119073742.png" alt="20180119073742"></a>

##Amazon開発者コンソール
いよいよAlexaスキルを作成していきます。まず[Amazon開発者コンソール](https://developer.amazon.com/home.html)にログインします。

上段メニュー**「ALEXA」**を選択して**「Alexa Skills Kit」**へ進みます。
<img width="400" alt="2018-01-17_22h23_05.png" src="https://qiita-image-store.s3.amazonaws.com/0/183744/1038bd1a-4e70-edaa-a410-72d27f1ce004.png">


右上の**「新しいスキルを追加する」**をクリックします。
<img width="400" alt="アマゾン アプリ 開発者ポータル.png" src="https://qiita-image-store.s3.amazonaws.com/0/183744/48da0e3f-8b2f-e03d-6279-377b770623cc.png">


スキルの基本情報として`言語``スキル名``呼び出し名`を入力して保存、次へ進みます。

- `言語`：スキルの対応言語。
- `スキル名`：このスキルの名前。ユーザーにはスキル名が表示される。
- `呼び出し名`：スキルを呼び出す時のフレーズ。「アレクサ、◯◯を開いて」の「◯◯」の部分。  
<img width="400" alt="アマゾン アプリ 開発者ポータル2.png" src="https://qiita-image-store.s3.amazonaws.com/0/183744/b01452ae-715a-ca99-ecf5-30afd4215f79.png">

続いて対話モデルを設定します。ここがAlexaスキル開発のポイントです。**「インテントスキーマ」**と**「サンプル発話」**を設定します。  
<img width="400" alt="アマゾン アプリ 開発者ポータル3.png" src="https://qiita-image-store.s3.amazonaws.com/0/183744/5468d26b-bfb4-1205-f4c8-d54a0fbaa458.png">

###インテントスキーマ
インテントスキーマは**「Alexaスキルで実行するアクションの種類」**です。JSON形式で以下のように入力します。

```json
{
  "intents": [
    {
      "intent": "GoOutTweet"
    },
    {
      "intent": "ComeHomeTweet"
    },
    {
      "intent": "AMAZON.HelpIntent"
    },
    {
      "intent": "AMAZON.StopIntent"
    },
    {
      "intent": "AMAZON.CancelIntent"
    }
  ]
}
```

- `GoOutTweet`：「行ってきます」に対応するアクション。
- `ComeHomeTweet`：「ただいま」に対応するアクション。
- `AMAZON.HelpIntent`：「使い方」や「説明」に対応するアクション。
- `AMAZON.StopIntent`：「中止」「停止」に対応するアクション。
- `AMAZON.Cancelntent`：「キャンセル」に対応するアクション。

`AMAZON.`と付いているインテントは**「標準インテント」**とよばれ、インテントスキーマに定義するだけでデフォルトのキーフレーズで使えるようになります。

一方で`GoOutTweet`、`ComeHomeTweet`は今回作成するAlexaスキルのオリジナルアクションである**「カスタムインテント」**です。カスタムインテントは次に説明する**サンプル発話でキーフレーズを設定する必要があります。**


###サンプル発話
サンプル発話ではカスタムインテントに対して**「そのインテントがアクションを起こすためのキーフレーズ」**を定義します。例えば「行ってきます」っぽいことを言えばGoOutTweetが、「ただいま」っぽいことを言えばComeHomeTweetが起動するイメージです。

今回はGoOutTweet、ComeHomeTweetに対して以下のように発話フレーズを設定します。

```
GoOutTweet 行く
GoOutTweet 行って
GoOutTweet 行きます
GoOutTweet 出かける
ComeHomeTweet ただいま
ComeHomeTweet 帰った
ComeHomeTweet 帰って
ComeHomeTweet 戻った
```

入力が完了したら次に進みます。

##AWS Lambda
ここで一度Amazon開発者コンソールを離れ、[AWS](https://aws.amazon.com/jp/)にログインします。

ログインしたらAWSサービスから**Lambda**を選択します。
<img width="400" alt="2018-01-17_23h05_39.png" src="https://qiita-image-store.s3.amazonaws.com/0/183744/d14c5cf4-0edc-5b6f-0960-41b9390ebb36.png">

右上の**「関数の作成」**を実行します。  
<img width="400" alt="Lambda Management Console.png" src="https://qiita-image-store.s3.amazonaws.com/0/183744/98d0755c-8320-0272-be51-aa891ce0827e.png">

作成フォームで**「一から作成」**を選択します。`名前`は任意の名前を入力し、`ランタイム`は**「Node.js 6.10」**を選択します。
<img width="400" alt="2018-01-17_23h37_08.png" src="https://qiita-image-store.s3.amazonaws.com/0/183744/63a7c1a8-b95e-d4cd-e790-3d8d5c1cb6ac.png">


続いて`ロール`の項目から**「カスタムロールの作成」**を選択します。
<img width="497" alt="2018-01-17_23h38_08.png" src="https://qiita-image-store.s3.amazonaws.com/0/183744/cdbea24e-2279-3649-e574-84456ecb10a7.png">


ロールの作成画面が表示されます。全てそのままで「許可」をクリックします。
<img width="400" alt="2018-01-17_23h40_49.png" src="https://qiita-image-store.s3.amazonaws.com/0/183744/14b49d15-3d53-8287-207f-e5cbdc38efc4.png">

ここまでできたら右下の「関数の作成」をクリック。

<br/>
実際に動かすプログラムを直接コーディングすることもできますが、今回はzipファイルをアップロードする形式を取ります。zipファイルとコードはgithub上にアップロードしています。
https://github.com/quotto/TweetByAlexa


画面の中頃に「関数コード」という項目があるので、ここで`コードエントリータイプ`**「.ZIPファイルをアップロード」**を選択して、ファイル`skill.zip`をアップロードします。
<img width="400" alt="2018-01-17_23h43_23.png" src="https://qiita-image-store.s3.amazonaws.com/0/183744/b0c6bc7a-3057-0eda-910b-55e8458e1145.png">

<a href="http://f.hatena.ne.jp/WorldWorldWorld/20180118221455"><img width="400" src="http://img.f.hatena.ne.jp/images/fotolife/W/WorldWorldWorld/20180118/20180118221455.png" alt="20180118221455"></a>


###一応プログラムの説明
`index.js`がメインの実行プログラムです。Amazon開発者コンソールで定義したインテントごとにAlexaの振る舞いを実装します。

```javascript:index.js
'use strict';

const Alexa = require('alexa-sdk');

const Twitter = require('twitter');

const APP_ID = undefined;  // TODO replace with your app ID (OPTIONAL).

const JSTOffset = 60 * 9 * 60 * 1000; // JST時間を求めるためのオフセット

const ErrorMessage = '<say-as interpret-as="interjection">ごめんなさい、</say-as>つぶやけませんでした。';

function calculateJSTTime() {
    var localdt = new Date(); // 実行サーバのローカル時間
    var jsttime = localdt.getTime() + (localdt.getTimezoneOffset() * 60 * 1000) + JSTOffset;
    var dt = new Date(jsttime);
    return dt;
}

const handlers = {
    'LaunchRequest': function () {
        this.emit('AMAZON.HelpIntent');
    },
    'GoOutTweet' : function() {
        var dt = calculateJSTTime();
        var stringTime = dt.getFullYear() + "年" + (dt.getMonth()+1) + "月" + dt.getDate() + "日 " + dt.getHours() + "時" + dt.getMinutes() + "分" + dt.getSeconds() + "秒 ";
        var message = stringTime + "に出かけました。（Alexaスキルでつぶやいています）";
        Twitter.postTweet(message).then(()=>{
            this.emit(':tell','つぶやきました。<say-as interpret-as="interjection">いってらっしゃい。</say-as>');
        },(error)=>{
            console.log(error);
            this.emit(':tell',ErrorMessage);
        })
    },
    'ComeHomeTweet' : function() {
        var dt = calculateJSTTime();
        var stringTime = dt.getFullYear() + "年" + (dt.getMonth()+1) + "月" + dt.getDate() + "日 " + dt.getHours() + "時" + dt.getMinutes() + "分" + dt.getSeconds() + "秒 ";
        var message = stringTime + "に帰ってきました。（Alexaスキルでつぶやいています）";
        Twitter.postTweet(message).then(()=>{
            this.emit(':tell','つぶやきました。<say-as interpret-as="interjection">おかえりなさい。</say-as>');
        },(error)=>{
            console.log(error);
            this.emit(':tell',ErrorMessage);
        })
    },
    'AMAZON.HelpIntent': function () {
        const speechOutput = '<say-as interpret-as="interjection">「いってきます」「ただいま」</say-as>、とつぶやくと、その時間をツイートします。';
        const reprompt = speechOutput;
        this.emit(':ask', speechOutput, reprompt);
    },
    'AMAZON.CancelIntent': function () {
        this.emit(':tell', this.t('STOP_MESSAGE'));
    },
    'AMAZON.StopIntent': function () {
        this.emit(':tell', this.t('STOP_MESSAGE'));
    },
};

exports.handler = function (event, context) {
    const alexa = Alexa.handler(event, context);
    alexa.APP_ID = APP_ID;
    // To enable string internationalization (i18n) features, set a resources object.
    alexa.registerHandlers(handlers);
    alexa.execute();
};
```

`Alexa.handler`の`this.emit`を実行することでAlexaが喋ります。

ツイッターに投稿する`Twitter.postTweet`の部分は同梱の`twitter.js`に自作のOAuthプログラムを実装しています。

```javascript:twitter.js
"use strict";

const AWS = require('aws-sdk');

const https = require('https');
const request = require('request');
const crypto = require('crypto');

const url='https://api.twitter.com/1.1/statuses/update.json';

function postTweet(message) {
    return new Promise((resolve,reject) => {
        const include_entities = {
            status: message,
            // include_entities: true
        };
        const params = {
            oauth_consumer_key: process.env.CONSUMER_KEY,
            oauth_token: process.env.ACCESS_TOKEN,
            oauth_signature_method: 'HMAC-SHA1',
            oauth_timestamp: (() => {
                const date = new Date();
                return Math.floor(date.getTime() / 1000);
            })(),
            oauth_nonce: (() => {
                const date = new Date();
                return date.getTime();
            })(),
            oauth_version: '1.0'
        };

        let auth_params = Object.assign(include_entities,params);

        let encoded_auth_params = Object.keys(auth_params).map(function(key){
            return `${encodeURIComponent(key)}=${encodeURIComponent(this[key])}`;
        },auth_params);
        encoded_auth_params.sort((a,b) => {
            if(a < b) return -1;
            if(a > b) return 1;
            return 0;
        });

        const sigunature_base = `${encodeURIComponent('POST')}&${encodeURIComponent(url)}&${encodeURIComponent(encoded_auth_params.join('&'))}`;

        const keyOfSign = `${encodeURIComponent(process.env.CONSUMER_SECRET)}&${encodeURIComponent(process.env.ACCESS_TOKEN_SECRET)}`;
        const signature = crypto.createHmac('sha1',keyOfSign).update(sigunature_base).digest('base64');
        params.oauth_signature = signature;

        let authorization = Object.keys(params).map(function(key) {
            return `${encodeURIComponent(key)}="${encodeURIComponent(this[key])}"`;
        },params);
        authorization.sort((a,b) => {
            if(a < b) return -1;
            if(a > b) return 1;
            return 0;
        });


        const headers = {
            Authorization: `OAuth ${authorization.join(', ')}`
        };

        const options = {
            url: url+"?status="+encodeURIComponent(message),
            headers: headers
        };

        request.post(options ,(error,response,body) => {
            if(error) {
                reject("reject:"+error);
            } else if(response.statusCode!=200) {
                reject("reject:statusCode="+response.statusCode);
            } else {
                resolve();
            }
        });
    });
};

module.exports.postTweet=postTweet;
```

`require('aws-sdk')`を実行することで、`process.env.CONSUMER_KEY`という形式で、後述する**AWS lambdaに設定した環境変数を利用することができます。**


<br/>
続いて**環境変数**を設定します。`CONSUMER_KEY`、`CONSUMER_SECRET`、`ACCESS_TOKEN`、`ACCESS_TOKEN_SECRET`をキーとして、TwitterApplicationManagementで取得したそれぞれの値を設定します。  
<img width="400" alt="2018-01-17_23h47_05.png" src="https://qiita-image-store.s3.amazonaws.com/0/183744/cd5e9506-0c43-561f-0738-d28cd8043500.png">


次に画面上段の「Designer」の`トリガーの追加`から**「Alexa Skills Kit」**を選択します。
<img width="400" alt="2018-01-17_23h49_04.png" src="https://qiita-image-store.s3.amazonaws.com/0/183744/f5614b45-782f-5bf7-91fa-1d1b6b864107.png">


画面最下部に「トリガーの追加」メニューが表示されるので**「追加」**を実行します。
<img width="400" alt="2018-01-17_23h51_09.png" src="https://qiita-image-store.s3.amazonaws.com/0/183744/deac4aad-fd9c-b6d1-c653-2c247af6011b.png">

最画面右上の「保存」を実行します。
<img width="433" alt="2018-01-17_23h54_49.png" src="https://qiita-image-store.s3.amazonaws.com/0/183744/6c8658bd-092e-d7ed-750d-87435546f61f.png">


<br/>
ここまでで関数の作成は完了です。正常に関数が動作するかをテストします。

「テスト」をクリックすると子画面が表示されるので、**「新しいイベントの作成」**を選びイベントテンプレート**「Alexa Start Session」**を選択します。
<img width="428" alt="2018-01-17_23h56_09.png" src="https://qiita-image-store.s3.amazonaws.com/0/183744/ee233491-44b1-469d-3a37-a362b6fd8e71.png">

<img width="400" alt="2018-01-17_23h57_34.png" src="https://qiita-image-store.s3.amazonaws.com/0/183744/d0b5b7a4-d051-80eb-ceec-e2bfd852e722.png">

すると何やらコードが表示されます。イベント名に適当な名前を入力し**「作成」**を実行します。

画面に戻り再び「テスト」を実行して**「実行結果：成功」**と表示されれば関数のテストは完了です。
<img width="547" alt="2018-01-18_00h06_23.png" src="https://qiita-image-store.s3.amazonaws.com/0/183744/ab586cee-8d9b-0162-5fde-3eaccffca970.png">


最後に画面右上に表示された**`ARN`の値をコピーしておきます。**この後の設定に利用します。  
<img width="375" alt="2018-01-18_00h10_28.png" src="https://qiita-image-store.s3.amazonaws.com/0/183744/66ad8cdf-a370-ff20-63df-2615a19ec867.png">

## Amazon開発者コンソールで仕上げとテスト
関数の作成が終わったらAmazon開発者コンソールに戻ります。

設定メニューの項目`エンドポイント`で**「AWS LambdaのARN（Amazonリソースネーム）」**を選択します。すると、下の方に`デフォルト`というフォームが表示されるので、**先ほどコピーしたARNを貼り付け**て次へ進みます。
<a href="http://f.hatena.ne.jp/WorldWorldWorld/20180118202603"><img width="400" src="http://img.f.hatena.ne.jp/images/fotolife/W/WorldWorldWorld/20180118/20180118202603.png" alt="20180118202603"></a>



テスト画面が表示されるので、ここで最終的な動作確認を行います。この画面では作成したAlexaスキルが起動した状態になっているため、発話サンプルで設定したキーフレーズを入力します。

フォームに**「ただいま」**と入力し、**「ツイログを呼び出す」**を実行します。
<a href="http://f.hatena.ne.jp/WorldWorldWorld/20180119005034"><img width="400" src="http://img.f.hatena.ne.jp/images/fotolife/W/WorldWorldWorld/20180119/20180119005034.png" alt="20180119005034"></a>

左側には**Alexaへ送信した入力情報**、右側にはAlexaが情報を受け取り**AWS Lambdaで実行された関数の結果**が表示されます。右側の`outputSpeech`がAlexaが喋る内容です。ここに想定通りのセリフが表示されていればOKです！  

##AmazonEchoに導入
ここまででAlexaスキルの作成は全て完了です。最後は**実機へ導入します。**

次の画面へ進むと「公開情報」の設定になります。今回は本当に公開するわけではないので、適当に入力します。  
<a href="http://f.hatena.ne.jp/WorldWorldWorld/20180118203311"><img width="400" src="http://img.f.hatena.ne.jp/images/fotolife/W/WorldWorldWorld/20180118/20180118203311.png" alt="20180118203311"></a>

<a href="http://f.hatena.ne.jp/WorldWorldWorld/20180118203315"><img width="400" src="http://img.f.hatena.ne.jp/images/fotolife/W/WorldWorldWorld/20180118/20180118203315.png" alt="20180118203315"></a>

<a href="http://f.hatena.ne.jp/WorldWorldWorld/20180118203339"><img width="400" src="http://img.f.hatena.ne.jp/images/fotolife/W/WorldWorldWorld/20180118/20180118203339.png" alt="20180118203339"></a>


最後に「プライバシーとコンプライアンス」の設定です。とりあえず**「いいえ」**と**「輸出コンプライアンス」**にチェックすればOKです。  
<a href="http://f.hatena.ne.jp/WorldWorldWorld/20180118203559"><img width="400" src="http://img.f.hatena.ne.jp/images/fotolife/W/WorldWorldWorld/20180118/20180118203559.png" alt="20180118203559"></a>


保存を実行すると画面左側のメニューから**「スキルのベータテスト」**がクリックできるようになります。  
<a href="http://f.hatena.ne.jp/WorldWorldWorld/20180118203654"><img width="400" src="http://img.f.hatena.ne.jp/images/fotolife/W/WorldWorldWorld/20180118/20180118203654.png" alt="20180118203654"></a>

**「テスターのメールアドレス」**に自分のアドレスを入力して追加します。他にも使ってもらいたい人がいれば最大500件まで登録できます。

最後に**「テストを開始」**を実行します。  
<a href="http://f.hatena.ne.jp/WorldWorldWorld/20180118203833"><img width="400" src="http://img.f.hatena.ne.jp/images/fotolife/W/WorldWorldWorld/20180118/20180118203833.png" alt="20180118203833"></a>


自分のアドレスにメールが届くので、その中のリンクをクリックします。なお日本語環境では2番目の**「JP customer～」リンク**を選択します。  
<a href="http://f.hatena.ne.jp/WorldWorldWorld/20180118203922"><img  width="400"  src="http://img.f.hatena.ne.jp/images/fotolife/W/WorldWorldWorld/20180118/20180118203922.png" alt="20180118203922"></a>


Alexaスキルのページ（アプリ画面）が表示されるので**「スキルテスト」**でスキルを有効化します。  
<a href="http://f.hatena.ne.jp/WorldWorldWorld/20180118204001"><img   width="400"  src="http://img.f.hatena.ne.jp/images/fotolife/W/WorldWorldWorld/20180118/20180118204001.png" alt="20180118204001"></a>

<a href="http://f.hatena.ne.jp/WorldWorldWorld/20180118204050"><img   width="400"  src="http://img.f.hatena.ne.jp/images/fotolife/W/WorldWorldWorld/20180118/20180118204050.png" alt="20180118204050"></a>

これで全て完了です！実際にAlexaに呼びかけてみましょう。

#次回
今回はACCESS_TOKENとACCESS_TOKEN_SECRETをベタ打ちにしたため、特定のTwitterアカウント専用のスキルとなりました。

この理由はAlexa Skills Kitがサポートする認可方式がOAuth2.0であるのに対して、TwitterAPI（の投稿）はOAuth1.0であるため、単純にアカウントリンクができないためです。

次はそれを解決するために中間の橋渡しをするサーバーを用意して、アカウントリンクバージョンを作ってみます。 

こちらに作成記事を公開しました！
https://qiita.com/quotto/items/865326ac1e7aa3ac9c3c