Alexa-hostedスキルでセッション永続性（S3）とメディアファイル（S3ファイル）を利用する方法

# はじめに
Alexaスキルを作成する際、2019年1月までは、「Amazon Developer Portal（Amazon.co.jpアカウント）」と「AWSアカウント」の2つを用意して、Alexaスキルを開発する必要がありましたが、2019年1月24日の[Alexa Blog](https://developer.amazon.com/ja/blogs/alexa/post/005f8b0c-1ef2-4be6-8211-90e3e8ef9679/alexa-hosted-skills-beta-is-now-available-to-all-developers-jp)の発表以降、「Amazon Developer Portal（Amazon.co.jpアカウント）」のみを用意すれば、Alexaスキルを作成することができるようになりました。

今回は、簡易的に、Alexa-hostedスキルでセッション永続性（S3）とメディアファイル（S3ファイル）を利用する方法を、サンプルソースを公開することで、お伝えしようと思います。

# 参考文献
- [Alexa-hostedスキルを使用してスキルをエンドツーエンドで作成する](https://developer.amazon.com/ja/docs/hosted-skills/build-a-skill-end-to-end-using-an-alexa-hosted-skill.html)
- [すべての開発者がAlexa-hostedスキル（ベータ版）を利用できるようになりました](https://developer.amazon.com/ja/blogs/alexa/post/005f8b0c-1ef2-4be6-8211-90e3e8ef9679/alexa-hosted-skills-beta-is-now-available-to-all-developers-jp)

# Alexaスキルの変更点
- 「スキル呼び出し名」を、「とっさの挨拶」に変更しています。

# コードエディタでの修正対象ファイル
- index.js
- package.json

# S3画像の用意
- S3バケット内のMediaフォルダ内に任意の画像をアップする必要があります。
- 画像アップロードの際に、パブリックアクセス権限つけず、そのままアップロードする必要があります。
- Amazon Developer Portal内の「コードエディタ」メニューから、「Media storage: S3」をクリックし、「バケット名/Media」フォルダに「icon_512_A2Z.png（任意ファイル）」ファイルを保存して利用します。
- S3画像のみ利用する場合は、package.jsonファイルの修正は、必ずしも必要ないと思います。

# セッション永続性（S3）利用時のポイント
- package.jsonファイルを修正し、"dependencies"項目の値に"ask-sdk-s3-persistence-adapter": "^2.0.0"を追記する必要があります。

# おことわり
- [Alexa-hostedスキルを使用してスキルをエンドツーエンドで作成する](https://developer.amazon.com/ja/docs/hosted-skills/build-a-skill-end-to-end-using-an-alexa-hosted-skill.html)の参考内容を元に、index.jsサンプルソースを作成しています。
- セッション永続性（S3）を、有効的に利用するためには、もっとプログラムを工夫する必要があります。プログラムの工夫は、みなさまにお任せいたします。

# index.jsサンプルソース

```javascript:index.js
/* eslint-disable  func-names */
/* eslint-disable  no-console */

// 参考サイト：https://developer.amazon.com/ja/docs/hosted-skills/build-a-skill-end-to-end-using-an-alexa-hosted-skill.html
// 参考サイト：https://gist.github.com/germanviscuso/70c979f671660fea811ccfb63801f936
// 参考サイト：https://dev.classmethod.jp/cloud/aws/ask-sdk-for-node-js-s3-persistence-adapter/
// 参考サイト：https://dev.classmethod.jp/cloud/ask-sdk-s3-persistence-adapter/
// 参考サイト：https://dev.classmethod.jp/cloud/alexa-sdk-v2-fourth/


/**
 * モジュール参照
 */
const Alexa = require('ask-sdk-core');
const Util = require('util.js');

// ask persistence adapterの読み込み
const persistenceAdapter = require('ask-sdk-s3-persistence-adapter');


/**
 * LaunchRequestHandler
 * ユーザ発話：「＜スキル名＞を開いて」
 */
const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'LaunchRequest';
    },
    async handle(handlerInput) {
        const { attributesManager } = handlerInput;

        // 永続化情報の取得
        const s3Attributes = await attributesManager.getPersistentAttributes() || {};

        let counterMessage = '';

        // 訪問回数にあわせた応答メッセージの設定
        if (!s3Attributes.counter) {
            s3Attributes.counter = 1;
            counterMessage = '初めてのご利用ありがとうございます。このスキルでは、';
        } else {
            s3Attributes.counter += 1;
            counterMessage = `${s3Attributes.counter}回目のご利用感謝です。では、`;
        }

        // TODO: S3バケット内のMediaフォルダに任意の画像をアップする必要があります。
        // 署名済み画像URL取得
        const pictureUrl = Util.getS3PreSignedUrl('Media/icon_512_A2Z.png');

        // S3永続化保存
        attributesManager.setPersistentAttributes(s3Attributes);
        await attributesManager.savePersistentAttributes();

        const speechText = `とっさの挨拶へようこそ。${counterMessage}英語のあいさつの練習をおこないます。<voice name="Joanna"><lang xml:lang="en-US">Hello.</lang></voice>とおっしゃってください。`;
        return handlerInput.responseBuilder
            .speak(speechText)
            .reprompt(speechText)
            .withStandardCard('とっさの挨拶', '英語であいさつしてね！', pictureUrl)
            .getResponse();
    }
};


/**
 * HelloWorldIntentHandler
 * ユーザ発話：「ハロー」
 */
const HelloWorldIntentHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'IntentRequest'
            && handlerInput.requestEnvelope.request.intent.name === 'HelloWorldIntent';
    },
    async handle(handlerInput) {
        const { attributesManager } = handlerInput;
        const s3Attributes = await attributesManager.getPersistentAttributes() || {};
        console.log('s3Attributes is: ', s3Attributes);

        // const counter = s3Attributes.hasOwnProperty('counter') ? s3Attributes.counter : 0;
        const counter = Object.prototype.hasOwnProperty.call(s3Attributes, 'counter') ? s3Attributes.counter : 1;

        const speechText = `お上手ですね、こんにちは。保存したカウンターは${counter}です。`;
        return handlerInput.responseBuilder
            .speak(speechText)
            // .reprompt('add a reprompt if you want to keep the session open for the user to respond')
            .getResponse();
    }
};


/**
 * HelpIntentHandler
 * ユーザ発話：「ヘルプ」
 */
const HelpIntentHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'IntentRequest'
            && handlerInput.requestEnvelope.request.intent.name === 'AMAZON.HelpIntent';
    },
    handle(handlerInput) {
        const speechText = 'このスキルでは、英語のあいさつの練習をおこないます。ハローとおっしゃってください。';

        return handlerInput.responseBuilder
            .speak(speechText)
            .reprompt(speechText)
            .getResponse();
    }
};


/**
 * CancelAndStopIntentHandler
 * ユーザ発話：「キャンセル」、「止めて」
 */
const CancelAndStopIntentHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'IntentRequest'
            && (handlerInput.requestEnvelope.request.intent.name === 'AMAZON.CancelIntent'
                || handlerInput.requestEnvelope.request.intent.name === 'AMAZON.StopIntent');
    },
    handle(handlerInput) {
        const speechText = '終了を受け付けました。ご利用ありがとうございます。';
        return handlerInput.responseBuilder
            .speak(speechText)
            .getResponse();
    }
};


/**
 * CancelAndStopIntentHandler
 * ユーザ発話：「終了」
 */
const SessionEndedRequestHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'SessionEndedRequest';
    },
    handle(handlerInput) {
        // Any cleanup logic goes here.
        return handlerInput.responseBuilder.getResponse();
    }
};


/**
 * IntentReflectorHandler
 * ユーザ発話：「＜カスタムインテントのサンプル発話（デバッグ用）＞」
 */
const IntentReflectorHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'IntentRequest';
    },
    handle(handlerInput) {
        const intentName = handlerInput.requestEnvelope.request.intent.name;
        const speechText = `デバッグモードです。${intentName}が呼び出されました。`;

        return handlerInput.responseBuilder
            .speak(speechText)
            // .reprompt('add a reprompt if you want to keep the session open for the user to respond')
            .getResponse();
    }
};


/**
 * ErrorHandler
 * エラーハンドラ
 */
const ErrorHandler = {
    canHandle() {
        return true;
    },
    handle(handlerInput, error) {
        console.log(`~~~~ Error handled: ${error.message}`);
        const speechText = 'ちょっと分かりませんでした。ハローとおっしゃってください。';

        return handlerInput.responseBuilder
            .speak(speechText)
            .reprompt(speechText)
            .getResponse();
    }
};


// アダプターを使用してスキルビルダーを初期化
const skillBuilder = Alexa.SkillBuilders.custom().withPersistenceAdapter(
    new persistenceAdapter.S3PersistenceAdapter({ bucketName: process.env.S3_PERSISTENCE_BUCKET })
);


/**
 * exports.handler
 * メイン処理
 */
exports.handler = skillBuilder
    .addRequestHandlers(
        LaunchRequestHandler,
        HelloWorldIntentHandler,
        HelpIntentHandler,
        CancelAndStopIntentHandler,
        SessionEndedRequestHandler,
        IntentReflectorHandler
    )
    .addErrorHandlers(ErrorHandler)
    .lambda();

```

# package.jsonサンプルソース

```javascript:package.json
{
  "name": "hello-world",
  "version": "0.9.0",
  "description": "alexa utility for quickly building skills",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Amazon Alexa",
  "license": "ISC",
  "dependencies": {
    "ask-sdk-core": "^2.0.7",
    "ask-sdk-model": "^1.4.1",
    "aws-sdk": "^2.326.0",
    "ask-sdk-s3-persistence-adapter": "^2.0.0"
  }
}

```

# おわりに
みなさん、いかがでしたでしょうか？
30分もあれば、Alexaスキル「Hello World」が完成するかと思います。
記事に誤り、不備等がございましたら、コメントにてご指摘いただけると幸いです。

2019/01/27 TAKAHIRO NISHIZONO