Alexa (Amazon echo)からツイッターに投稿．API Gateway + LambdaでOAuth認証

# Alexaのスキルからツイッターに投稿

Amazon echo(以下アレクサ)のスキルからツイッターに投稿するサンプルプログラムを作成しました．
投稿するには次の設定と実装が必要です．本記事ではこれらの一連の流れを紹介したいと思います．

- ツイッターの認証(OAuth認証)
 - アレクサのアカウントリンクにはOAuth2.0認証が必要です．しかしながらツイッターはOAuth1.0認証です．この違いを吸収するためにトンネリングする仕組みを作ります．今回はAPI GatewayとAWS Lambdaを使用したサーバレスアーキテクチャで実装します．
- アカウントリンクの設定
 - アレクサスキルから外部のアカウント(Twitterやfacebook等)と連携するには[アカウントリンク](https://developer.amazon.com/ja/blogs/alexa/post/6df1f491-30f5-4451-b0da-bcd8f0a06a5c/chapter5-jp)という機能を使用します．
- 投稿

なお2018年7月31日現在，アレクサは長文の聞き取りが苦手なので，ツイートする文章すべてをユーザに発言させるのではなく，何らかの単語だけを聞いて，文章を生成するほうが良いかと思います．

# 構成
## インターフェイス
![Untitled Diagram.png](https://qiita-image-store.s3.amazonaws.com/0/73358/58285f5b-bbbf-2247-590c-9e0a3c0e0560.png)

API Gatewayにトンネリングサーバの機能を担わせます．API GatewayのAPIには次のエンドポイントを用意します．

- トークンの取得リクエスト
- Twitterからのコールバック先

そして，Twitterからはaccess_tokenとaccess_token_secretの2つがコールバック先に送られてきて，それ以降の認証では両方必要ですが，アレクサ側はaccess_tokenとして1つしか記録できません．そこで，この2つを結合してaccess_tokenとして記録します．利用する際は，2つを都度分割してツイッターに送信します．（このあたりは[AlexaスキルでTwitterのアカウントリンクを行う](https://qiita.com/quotto/items/865326ac1e7aa3ac9c3c)を参考にさせていただきました．）


## 言語

- TypeScript(node.js)

## 使用するコンポーネント
- Twitter API
- Alexa Skill
- API Gateway
- Lambda
- ( Parameter Store )

# 完成形
完成形をGithubで公開しております．ご参考にしていただければ幸いです．

- https://github.com/hiroyky/alexa_tweet

# 作成

## 前提

アレクサスキルを作成するために[アレクサのデベロッパーコンソール](https://developer.amazon.com/alexa/console/ask)でスキル開発を始めましょう．
はじめてのスキル開発の場合，[公式の開発トレーニング](https://developer.amazon.com/ja/alexa-skills-kit/training/building-a-skill)がわかりやすいです．

## ツイッターのアプリ作成
[Twitter Developer](https://developer.twitter.com/en/apps)で新規ツイッターアプリを作成します．

### 備考
2017年7月現在, apps.twitter.com から developer.twitter.com へ移行期のようです．また2017年7月からアプリの新規作成には認証・審査が必要になったみたいです．

### パーミッション
アクセスパーミッションは「Read and write」とします．
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73358/7458d40d-83a8-d09a-0adc-108789701768.png)

## Consumer API Key
Consumer API Keysが作成されています．ユーザがアプリを認証する際に必要になります．大切なキーですのでAPI Key, API secret keyを流出させないようにしましょう．
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73358/cbe94e87-4819-d8e2-840d-152c3cb4e962.png)

- （開発時点での私のキーを記載しておりますが，既に無効になっております．）

## 認証APIの作成

### AWS Lambdaの作成

AWS Lambda用のOAuth認証用のエンドポイントを作成します．下記のようなコードをコンパイルしてLambdaを作成します．

[aws-serverless-express](https://github.com/awslabs/aws-serverless-express)を導入することでLambda上でもexpressを利用しています．Lambdaのコードとはいえ，いつものnode.jsのWebサービスと似たコードになってるかと思います．

```TypeScript
import { APIGatewayEvent, Callback, Context } from "aws-lambda";
import * as awsServerlessExpress from "aws-serverless-express";
import cookieParser from "cookie-parser";
import express from "express";
import { checkSchema, validationResult, ValidationSchema } from "express-validator/check";
import { OAuth } from "oauth";

const app = express();
app.use(cookieParser());

app.get("/oauth/request_token", async (req, res, next) => {
    const oauth = new OAuth(
        "https://api.twitter.com/oauth/access_token",
        "https://api.twitter.com/oauth/authenticate",
        "K0sazMebqaqO7eTMjxVFlT1Lh", // API KEY
        "avSTp3mKuJUXmkRumJqDp8XOA5l7zGuQpD7BO2Kmi6EyyulJf9", // API Secret key
        "1.0",
        "", // callback url
        "HMAC-SHA1");

    oauth.getOAuthRequestToken((err, token, secret, result) => {
        if (err) {
            console.error(`error: ${JSON.stringify(err)}`);
            res.status(500).end(JSON.stringify(err));
            return;
        }

        const url = `https://api.twitter.com/oauth/authenticate?oauth_token=${token}`;
        res.cookie("secret", secret);
        res.cookie("redirect", req.query.redirect_uri);
        res.cookie("state", req.query.state);
        res.cookie("client", req.query.client_id);
        res.redirect(url);
    });
});


const callbackSchema: ValidationSchema = {
    client: { in: ["cookies"] },
    redirect: { in: ["cookies"] },
    secret: { in: ["cookies"] },
    state: { in: ["cookies"] },
    oauth_token: { in: ["query"] },
    oauth_verifier: { in: ["query"] },
};

app.get("/oauth/callback",
    checkSchema(callbackSchema),
    async (req, res, next) => {
        if (!validationResult(req).isEmpty()) {
            res.state(400).end();
            return;
        }

        const oauth = new OAuth(
            "https://api.twitter.com/oauth/access_token",
            "https://api.twitter.com/oauth/authenticate",
            "K0sazMebqaqO7eTMjxVFlT1Lh", // API KEY
            "avSTp3mKuJUXmkRumJqDp8XOA5l7zGuQpD7BO2Kmi6EyyulJf9", // API Secret key
            "1.0",
            req.cookies.redirect,
            "HMAC-SHA1");

        oauth.getOAuthAccessToken(req.query.oauth_token, req.cookies.secret, req.query.oauth_verifier, (err, token, secret) => {
            if (err) {
                res.status(500).end(JSON.stringify(err));
                return;
            }
            const url = `${req.cookies.redirect}#state=${req.cookies.state}&access_token=${token},${secret}&client_id=${req.cookies.client}&token_type=Bearer`;
            res.redirect(url);
        });
    });

const server = awsServerlessExpress.createServer(app, undefined, ["application/json"]);
export function handler(event: APIGatewayEvent, context: Context, callback: Callback) {
    awsServerlessExpress.proxy(server, event, context);
}
```

- API Keyは適宜書き換えてください．なお，API Keyをハードコーディングした場合，安全のためにgitでバージョン管理することは控えたほうが良いと思います．


#### /oauth/request_token

アレクサスキルの管理アプリでリクエストするエントリポイントです．アレクサスキルからクエリパラメータとして，クライアントID, アレクサで設定したリダイレクトURL，ステートなどが入力されます. oauth.getOAuthRequestToken()で取得したtoken, secretを取得し，ユーザが連携承認後のリダイレクト先で必要な情報(secret, クライアントID, アレクサで設定したリダイレクトURL，ステート)をクッキーに保存した上で，ツイッターの認証ページにリダイレクトしています．

- なお，OAuthのコンストラクタ引数'callback url'は後ほど指定します．


#### /oauth/callback
ツイッターの認証ページでユーザがOKしたらツイッターからリダイレクトされるエントリポイントです．ブラウザのクッキーから/oauth/request_tokenで保存したクッキー内容を読み出して，アレクサスキル用のURLを作成してリダイレクトしています．
なお，``checkSchema(callbackSchema)``では，必要な項目がクッキーに含まれているかを本体関数を実行する前に一括して確認しています．不足があれば400エラーを返します．
[こちらのやり方](https://qiita.com/quotto/items/865326ac1e7aa3ac9c3c#じゃあどうするの)と同様にtokenとsecretをコンマで結合して一つのaccess_tokenとして登録しています．

### API Gatewayの作成

作成したLambdaの呼び出しインターフェイスとしてREST APIをAPI Gatewayで作成します．

Lambdaのエントリポイントのパスと同様の構成になるようにリソースを作成し，Lambdaと紐づけます．
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73358/94d466b2-7030-0741-cd0c-5dce65102ac1.png)

一通り作業が完了したらデプロイして反映します．
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73358/0e378509-2880-875a-49c2-2d36e68a6604.png)

デプロイするとURLが発行されます．callbackのURLを確認し，Lambdaのコードのcallback urlに指定します.
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73358/e3505260-7540-1c72-2241-4892384fc44f.png)

```typescript
    const oauth = new OAuth(
        "https://api.twitter.com/oauth/access_token",
        "https://api.twitter.com/oauth/authenticate",
        "K0sazMebqaqO7eTMjxVFlT1Lh", // API KEY
        "avSTp3mKuJUXmkRumJqDp8XOA5l7zGuQpD7BO2Kmi6EyyulJf9", // API Secret key
        "1.0",
        "", // callback url
        "HMAC-SHA1");
```

- 本来はきちんとドメインを発行して，コールバックURLを作成すべきところですが，今回はAPI Gatewayが発行するランダムなホスト名のURLにします．

## アカウントリンクの設定

アレクサスキルの設定画面からアカウントリンクの設定を開きます．

![image.png](https://qiita-image-store.s3.amazonaws.com/0/73358/fd70cd9c-fc41-d783-9fe0-1101f9bd04b9.png)

次の手順を行います．
1. アカウントリンクの作成を許可をオンにする．
2. Authorization Grantの種別を「Implicit Grant」を選びます．
3. 認証画面URIには先程のAPI Gatewayのエンドポイントのうち，/oauth/request_tokenのURLを指定します．

## Callback URLの設定
Twitterのアプリ設定ページを開いてコールバックURLに，API Gatewayの/oauth/callbackのURLを指定します．２つ以上指定する必要があるようなので，API Gatewayのステージを分けて登録しました．

![image.png](https://qiita-image-store.s3.amazonaws.com/0/73358/e0d81cb1-880d-f77a-4039-52a309a401fb.png)

## アレクサスキル本体の作成
認証周りに続いて，ツイッターにツイートするスキル本体を実装します．

### インテントの定義
アレクサの設定画面で，「つぶやいて」という発話に対して，TweetRequestというインテントを実行するようにします．

![image.png](https://qiita-image-store.s3.amazonaws.com/0/73358/16db8984-6ff9-b8c1-3739-fd9db09561dd.png)


### Lambdaの作成
スキルのLambdaを作成します．

TweetRequestインテントでツイート処理を行っています．こちらも現段階ではツイッターのキーをハードコーディングしているので，まだgitで管理すべきではありません．

access_tokenはtoken, secretをコンマ区切りで結合したため，分割を行っています．

```TypeScript
import Alexa from "alexa-sdk";
import Twitter from "Twitter";

export function handler(event: Alexa.RequestBody<any>, context: Alexa.Context) {
    const alexa = Alexa.handler(event, context);
    alexa.registerHandlers(handlers);
    alexa.execute();
}

const handlers: Alexa.Handlers<any> = {
    LaunchRequest: function() {
    },

    SessionEndedRequest: function() {
        this.emit(":tell", "終了します．");
    },

    TweetRequest: async function() {
        const accessToken = this.event.session.user.accessToken;
        if (accessToken === undefined) {
            this.emit(":tellWithLinkAccountCard", "ログインしてください");
            return;
        }

        const client = new Twitter({
            consumer_key: "K0sazMebqaqO7eTMjxVFlT1Lh", // API KEY            
            consumer_secret: "avSTp3mKuJUXmkRumJqDp8XOA5l7zGuQpD7BO2Kmi6EyyulJf9", // API Secret key
            access_token_key: accessToken.split(",")[0], // access, secretを分割
            access_token_secret: accessToken.split(",")[1], // access, secretを分割
        });

        const message = `テスト ${new Date().getTime() / 1000}`;
        client.post("statuses/update", {status: message}, (err: Error, tweet: any, response: any) => {
            if (err) {
                console.error(err);
                console.error(response);
                this.emit(":tell", "つぶやきに失敗しました");
            }
            this.emit(":tell", "つぶやきました");
        });
    },
};
```

## 動作確認
ここまでで一通り動作するかと思います．
### アカウントリンク
[スキルの管理画面](https://alexa.amazon.co.jp/spa/index.html#skills/your-skills/?ref-suffix=ysa_gw)もしくはアレクサの管理スマートフォンアプリからスキルを有効化します．有効化する際にアカウントリンクが施行され，ツイッターの認証画面が表示されます．

![image.png](https://qiita-image-store.s3.amazonaws.com/0/73358/0178654b-8a9c-5524-7cbd-7f8e90689c64.png)

### スキル実行

テスト画面か実際のアレクサで「<スキル名>つぶやいて！」と言えば自身のツイッターアカウントにテストツイートが実行されると思います．

## 仕上げ

### APIキーの管理と取得，環境変数
これまでツイッターのAPIキーをハードコーディングしていましたが，当然ながら危険で，gitで管理できる状態ではありません．そこでAWSのSystems Managerの[Parameter Store](https://ap-northeast-1.console.aws.amazon.com/systems-manager/parameters)というサービスを使って管理を行います．登録すればAWS SDKを利用して取得できます．(IAMでLambdaに該当パラメータにアクセスする権限(ssm:GetParameter)を与える必要があります．)

キーの取得は以下のような実装で行います．

```TypeScript
import { SSM } from "aws-sdk";

export default class Env {
    protected ssm = new SSM();
    protected async getSSMParameter(name): Promise<string> {
        const param: SSM.GetParameterRequest = {
            Name: name,
            WithDecryption: true,
        };
        const result = await this.ssm.getParameter(param).promise();
        if (result === undefined) {
            throw new Error(`failed to get SSM parameter: ${name}`);
        }
        return result.Parameter.Value;
    }
}
```

実際には，[env.ts](https://github.com/hiroyky/alexa_tweet/blob/master/src/env.ts)ように実装して，環境変数と合わせてAPIキーを取得しています．

そして，取得側では次のように変更します．

```typescript
const env = new Env();
await env.init();
const client = new Twitter({
    consumer_key: env.twitter.oauth.key,            
    consumer_secret: env.twitter.oauth.secret,
    access_token_key: accessToken.split(",")[0],
    access_token_secret: accessToken.split(",")[1],
});
```

### デプロイの定義

Lambda, API Gateway, IAMをまとめて構築するために，CloudFormationを利用します．
構築方法の定義を[sam.yml](https://github.com/hiroyky/alexa_tweet/blob/master/deploy/sam.yml)に書きました．

Parameter Store以外をまとめて構築できます．

```shell
aws cloudformation package --template-file deploy/sam.yml --s3-bucket ${S3バケット名} --output-template-file ./artifacts/sam.yml 
aws cloudformation deploy --template-file ./artifacts/sam.yml --stack-name alexa-tweet --capabilities CAPABILITY_NAMED_IAM
```


# まとめ

アレクサのスキルからツイッターにツイッターするために，

- OAuth認証のトンネリングをAPI Gateway + Lambdaで作成
- ツイートするアレクサスキルのLambdaを作成
- Twitter及びアレクサスキルの設定

を行いました．

# 参考文献
- https://www.google.com/search?q=alexa%20twitter
- https://qiita.com/quotto/items/865326ac1e7aa3ac9c3c