みんなの足跡カウンター作ってみました

![logo.png](https://qiita-image-store.s3.amazonaws.com/0/96385/7b2a296d-1ec1-0a9e-5b85-1f613527b29c.png)

#何それ
昔blogや個人のウェブサイトでよく見かけたアクセスカウンターをスキルで再現してみました。
スキルを呼び出すと何番目のアクセスかを言いますが、せっかくなので、キリ番（キリのいい数字）をゲットしたらそれもフィードバックさせます。
※有名なサイトのキリ番なんか、たまたま踏むと恐れ多かったり、コメント残さないと踏み逃げとか言われたり、独特の文化がありましたよね

ちょうどいい記事がありましたのでついでにご紹介です。
[ITmediaさんの「アクセスカウンタ」と「キリ番」の思い出](http://www.itmedia.co.jp/pcuser/articles/1612/08/news014.html)

一から開発してスキル申請するところまで、1日でできるほどシンプルです。
大したことしてませんが、簡単なマルチプレイスキルのご参考までに中身を解説してみます。

#用意するもの
・Alexa(Amazon Developer)アカウント
・AWSアカウント

作り方の99%は@zono_0さんのこちらを参考にしています。
細かい手順は割愛します（手抜き
[https://qiita.com/zono_0/items/57247bed70dbe2b3ce28](https://qiita.com/zono_0/items/57247bed70dbe2b3ce28)

#概要図
<img width="1032" alt="スクリーンショット 2018-07-14 12.56.30.png" src="https://qiita-image-store.s3.amazonaws.com/0/96385/2aa54120-10a8-a912-76e3-e794e61d50c1.png">


#Alexa(Amazon Developer)でやること
必須の4項目です。簡単にさらっと説明します。
・呼び出し名の設定
・インテントの設定
・ビルド
・エンドポイントの設定（ARNを指定するだけです）

##呼び出し名
「みんなの足跡カウンター」にしました。
（あとで修正が効かないことに気づき後悔しましたが・・・）
<img width="455" alt="スクリーンショット 2018-07-14 14.22.57.png" src="https://qiita-image-store.s3.amazonaws.com/0/96385/b5e91d00-ae42-cf00-1ff0-a7f10d8207d6.png">


##インテント
最低でも1つのインテントとサンプル発話が必要らしいので作りました。
<img width="797" alt="スクリーンショット 2018-07-14 14.24.57.png" src="https://qiita-image-store.s3.amazonaws.com/0/96385/7ede3a89-1b8f-358f-ce7b-03afe21bc48f.png">

サンプル発話のところ、多分この入れ方は間違っていると思われます。
理由はあとで・・・。

スキルを呼び出すだけで使えるので、スロットは特にありません。

##エンドポイント（AWS側の準備が終わった後にやります）
AWSのLambdaのARNを指定します。
Alexaとバックエンドの接続がこれだけでできちゃうってほんと楽です。
Alexa側はARN、Lambda側はスキルIDをそれぞれ設定します。
（ARNの設定は次に書いてあるAWSの準備が終わってからになります）


<img width="954" alt="スクリーンショット 2018-07-14 14.32.40.png" src="https://qiita-image-store.s3.amazonaws.com/0/96385/b62ec4c7-0c58-5560-1f08-c488e2716a49.png">

#AWSでやること
ちょっとした設定とLambdaのコーディング、DynamoDBの準備くらいです。

Alexa Skills Kit側には先ほどのスキルIDを入れます。
<img width="1248" alt="スクリーンショット 2018-07-14 14.52.22.png" src="https://qiita-image-store.s3.amazonaws.com/0/96385/c3953c17-1725-a406-b93f-d4b3b5013c31.png">

##Lambdaの中身
ソースはこんな感じです。
恥ずかしいポイント①：キリ番は計算じゃなくてベタ打ち（まず1000なんて行かんしw
恥ずかしいポイント②：that=thisとか使っちゃった
恥ずかしいポイント③：エラーハンドリングほとんどしてない（スロット使ってないから大丈夫だと思うけど。。。
恥ずかしいポイント④：ログ出力とかそのまま

```ソース見られるの恥ずかしい.js
'use strict';
const Alexa = require('alexa-sdk');
const AWS = require('aws-sdk');

const APP_ID = "NAISHO";

const SKILL_NAME = 'みんなの足跡カウンター';
const MESSAGE_PREFIX = "あなたは";
const MESSAGE_SUFFIX = "番目の訪問者です！";
const HELP_MESSAGE = '「みんなのカウンターを開いて」と言ってね';
const HELP_REPROMPT = HELP_MESSAGE;
const STOP_MESSAGE = 'またね!';
const ERROR_MESSAGE = "変なエラーがでちゃいました。ごめんなさい";
const KIRIBAN_MESSAGE = "おめでとうございます！キリ番ゲットです！";

var docClient = new AWS.DynamoDB.DocumentClient();
let that;

const kiriban = {
    10: true,
    11: true,
    20: true,
    22: true,
    30: true,
    33: true,
    44: true,
    50: true,
    55: true,
    60: true,
    66: true,
    70: true,
    77: true,
    80: true,
    88: true,
    90: true,
    99: true,
    100: true,
    111: true,
    123: true,
    200: true,
    222: true,
    300: true,
    333: true,
    400: true,
    444: true,
    500: true,
    555: true,
    600: true,
    666: true,
    700: true,
    777: true,
    800: true,
    888: true,
    900: true,
    999: true,
    1000: true,
    1111: true,
    1234: true
};

const handlers = {
    'LaunchRequest': function () {
        console.log("launchしたよ");
        this.emit('GetOurCounterIntent');
    },
    'GetOurCounterIntent': function () {
        that = this;
        // ユーザー別のカウント取得
        const kCounter = "ashiato";
        let counter = this.attributes[kCounter];
        
        // なければ0から
        if(counter === undefined){
            counter = 0;
        }
        
        // 1カウントアップしてdynamodbへ反映
        counter = counter + 1;
        this.attributes[kCounter] = counter;

        // 全体カウント
        let gcounter;
        
        // -------
        // DB更新
        // -------
        var updateParams = {
            TableName: "テーブルの名前",
            Key: {
                "userId":"TotalCounter"
            },
            UpdateExpression: "SET mapAttr.ashiato = mapAttr.ashiato + :m",
            ExpressionAttributeValues: { 
                ":m": 1
            },
            ReturnValues: "UPDATED_NEW"
        };
        
        console.log("updateparams:" + JSON.stringify(updateParams));
        
        docClient.update(updateParams, function(err, data) {
            if (err) {
                // エラーになった
                console.log("write-error:" + JSON.stringify(err, undefined, 2));
                that.emit('Unhandled');

            } else {
                // 全体のカウント取得成功
                // console.log("write-success:" + JSON.stringify(data, undefined, 2));
                gcounter = data.Attributes.mapAttr.ashiato;
                // console.log( "gcounter:" + gcounter);

                let speechOutput = MESSAGE_PREFIX + gcounter + MESSAGE_SUFFIX;

                console.log("kiriban[gcounter]:" + kiriban[gcounter]);
                console.log(typeof(kiriban[gcounter]));

                // もしキリ番ならコメント追加
                if(kiriban[gcounter]){
                    speechOutput = speechOutput + KIRIBAN_MESSAGE;
                }
                console.log("gcounter:" + gcounter);
                console.log("スピーチ：" + speechOutput);
        
                that.response.cardRenderer(SKILL_NAME, speechOutput);
                that.response.speak(speechOutput);
                that.emit(':responseReady');
                // that.emit(':tellWithCard', speechOutput, SKILL_NAME, speechOutput);
    
            }
        });
        
        // console.log(this);
    },
    'AMAZON.HelpIntent': function () {
        const speechOutput = HELP_MESSAGE;
        const reprompt = HELP_REPROMPT;

        this.response.speak(speechOutput).listen(reprompt);
        this.emit(':responseReady');
        // this.emit(':ask', speechOutput, reprompt);
    },
    'AMAZON.CancelIntent': function () {
        const speechOutput = STOP_MESSAGE;
        this.response.speak(speechOutput);
        this.emit(':responseReady');
        
        // this.emit(':tell', STOP_MESSAGE);
    },
    'AMAZON.StopIntent': function () {
        const speechOutput = STOP_MESSAGE;
        this.response.speak(speechOutput);
        this.emit(':responseReady');
        // this.emit(':tell', STOP_MESSAGE);
    },
    'Unhandled': function (){
        const speechOutput = ERROR_MESSAGE;
        this.response.speak(speechOutput);
        this.emit(':responseReady');
    }
};

exports.handler = function (event, context, callback) {
    const alexa = Alexa.handler(event, context, callback);
    alexa.dynamoDBTableName = 'OurCounterTable'; 
    alexa.APP_ID = APP_ID;
    alexa.registerHandlers(handlers);
    alexa.execute();
};
```

ユーザー毎のカウントはalexa-sdkを使ってDynamoDBに永続化しています。
全体のカウントはaws-sdkを使って、永続化しているのと同じテーブルに
あらかじめ全体カウント用レコードを作っておいてカウントを保持しています。

##DynamoDBの中身
上の結果、DynamoDBのテーブルはこうなります。
TotalCounterはあらかじめ追加しておいたレコードです。
それ以外はユーザー毎のレコードです。
<img width="637" alt="スクリーンショット 2018-07-14 15.07.26.png" src="https://qiita-image-store.s3.amazonaws.com/0/96385/02aa4806-a5fd-0796-6de2-325ddc3b5eca.png">

1回のスキル実行で、ユーザーごとのカウンターに1足すのと同時に
全体のカウンター（TotalCounter）にも1を足してます。

本当はTotalCounterなんて作らず毎回計算してもいいはずです。
ただDynamoDBがSQL使えないのでSUMする方法がすぐにわからなかったことと、
ユーザー増えたら処理重くなりそうだったので、この方法にしました。

DynamoDBはAWS-SDKを使ってます。パラメーターはソースのここの部分です。

```パラメーター.js
        var updateParams = {
            TableName: "テーブルの名前",
            Key: {
                "userId":"TotalCounter"
            },
            UpdateExpression: "SET mapAttr.ashiato = mapAttr.ashiato + :m",
            ExpressionAttributeValues: { 
                ":m": 1
            },
            ReturnValues: "UPDATED_NEW"
        };
```
TableName:テーブル名
key: レコードを特定するためのキー
UpdateExpression: mapAttr.ashiatoに1足すという式を定義してます
ExpressionAttributeValues:　式に出てくる変数の値を指定できるぽいです「:m」に1を入れてるので1ずつ増えます
ReturnValues:設定すると更新対象となったレコード（TotalCounter）を返り値として得られました

呼び出し部分のソースはここです。

```呼び出し部分.js
       docClient.update(updateParams, function(err, data) {
            if (err) {
                // エラーになった
                console.log("write-error:" + JSON.stringify(err, undefined, 2));
                that.emit('Unhandled');

            } else {
                // 全体のカウント取得成功
                // console.log("write-success:" + JSON.stringify(data, undefined, 2));
                gcounter = data.Attributes.mapAttr.ashiato;
                // console.log( "gcounter:" + gcounter);

                let speechOutput = MESSAGE_PREFIX + gcounter + MESSAGE_SUFFIX;

                console.log("kiriban[gcounter]:" + kiriban[gcounter]);
                console.log(typeof(kiriban[gcounter]));

                // もしキリ番ならコメント追加
                if(kiriban[gcounter]){
                    speechOutput = speechOutput + KIRIBAN_MESSAGE;
                }
                console.log("gcounter:" + gcounter);
                console.log("スピーチ：" + speechOutput);

                that.response.cardRenderer(SKILL_NAME, speechOutput);
                that.response.speak(speechOutput);
                that.emit(':responseReady');
                // that.emit(':tellWithCard', speechOutput, SKILL_NAME, speechOutput);

            }
        });
```

#そのうちやること
・何度でもキリ番が取れるようにする
・ランキングとか入れてみる（まさにマルチプレイ
・Alexaのお祝いパターン増やす。東北訛りを表現してみたいw
・キリ番を自動計算する

#後悔したこと
**「呼び出し名」が「スキル名」と一緒じゃなくても良いなら・・間違いなく「略称」（みんなの足跡カウンター：アシカンみたいな）呼び出し名にした方がいいです。**
何度も繰り返し使うスキルの名前が長いとマジで使う気なくなるし、略称こそスキル名を覚えてもらうのに必要だと思います。


何かご質問、ご意見などあればTwitterか下のコメント欄からコメントください！