夢野久作の ”瓶詰地獄” がサーバレスアーキテクチャの Azure Functions で送出されて3つの瓶が流れ着く LINEBOT

# はじめに
探偵小説作家夢野久作の [瓶詰地獄（青空文庫リンク）](http://www.aozora.gr.jp/cards/000096/files/2381_13352.html) は、昭和3年に書かれた書簡体形式の掌編です。
「三個とも封瓶のまま」打ち上げられた手紙を、読み進めていくと、最後に劇的な読後感に襲われる仕掛けになっています。

![btl_typeB.png](https://qiita-image-store.s3.amazonaws.com/0/121307/f1085d48-9e67-7e80-78a6-c1ff9b00b58d.png)




# 作中手紙の ”書かれた順番”
”書かれた順番”　については、
> この作品における3篇の手紙が、第3、第2、第1の瓶の内容の順番で書かれたとする（[Wikipedia](https://ja.wikipedia.org/wiki/%E7%93%B6%E8%A9%B0%E3%81%AE%E5%9C%B0%E7%8D%84) ）

解釈が一般的なものです。

が、**読者が読む順番（情報の与えられる順番）** がそもそも違ったら、”書かれた順番”について、どんな印象を持てるでしょうか…？

#作中手紙を ”読む順番” 
 ”読む順番” が違うとどんな気持ちになるか、気になったので、タイトル要素で実装しました！（本題）
画面イメージは下記です。（”瓶詰地獄”ネタバレ防止のため、本文一部が表示されてしまう操作動画アニメGIFは本稿末尾に配置しました…）

<img width="231" alt="gamenimg.JPG" src="https://qiita-image-store.s3.amazonaws.com/0/121307/21c6d892-d7a5-598e-61ca-f1d925f69e92.jpeg">

操作の流れ
1.　BOTにテキストを投げる
2.　浜辺の絵と、カルーセルに載った瓶がBOTから返ってくる
3.　カルーセルの中にはランダムな順番で、作中の3つの手紙が詰まっている
4.　「瓶を開ける」ボタンをクリックすると、手紙本文画像が開く


「封を開けて読む」読書体験をしたかったので「瓶画像をクリックしたら、手紙が開く」2段階UIにしました。

# 環境（2017年4月現在）
UI：　Line-BOT
静的コンテンツ（画像）配信：　Azure BLOB ストレージ
Webhook：　Azure Functions

# UI：LineBOT 
Line Messaging API は、去年の春（2016年春）のトライアル提供から、秋の正式提供への経緯を経ています。
その影響か各種仕様変更が多々あり、Line側準備は公式サイトの最新情報を見るのが一番だと思います。
[ここ](https://developers.line.me/messaging-api/getting-started) から [ここ](https://devdocs.line.me/ja/#messaging-api) など。

#静的コンテンツ（画像）配信：　Azure BLOB ストレージ
前述のLineBotの細かい仕様変更のひとつに、Line へ reply/push できるコンテンツは **https に限定** するというものがあります。
（去年はたしか http でよかったような…?）
自前 https 環境設定は面倒なので、Azure BLOB ストレージ に画像ファイルを置いて、静的配信することにしました（[参考](https://docs.microsoft.com/ja-jp/azure/storage/storage-manage-access-to-resources)）。

# Webhook：　Azure Functions
去年リリースされた（参考 [techcrunch] [ MicrosoftがAWSのLambdaに続いてサーバー不要のイベント駆動型クラウドサービスAzure Functionsをローンチ](http://jp.techcrunch.com/2016/04/01/20160331microsoft-answers-aws-lambdas-event-triggered-serverless-apps-with-azure-functions/)） Azure Functions は 従来あった webJobs を lamda ライクに拵えたサービスです。
今回は、node.js で下記のように書きました。

（httpリクエストトリガー：　リクエストをキューに溜める）

```js:HTTPTrigger
module.exports = function (context, req) {
  context.log('JavaScript HTTP trigger function processed a request. RequestUri=%s', req.originalUrl);
  context.log(req);
  context.bindings.outputQueueItem = req.body;
  res = { body : "" };
  context.done();
};
```
（キュートリガー：　キュー起動で処理実行）

```js:QueueStoragetrigger
var https = require("https");
var url = require("url");

// 送出
function send_line(postData, context, mtd){
    var parse_url = url.parse('https://api.line.me/v2/bot/message/'+ mtd);
    var options = {
    host: parse_url.host,
    path: parse_url.path,
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer {（キー）}'
    }
    };
    var req = https.request(options, function(res) {
    context.log('-- send_line : STATUS: ' + res.statusCode);
    context.log('-- send_line : HEADERS: ' + JSON.stringify(res.headers));
    res.setEncoding('utf8');
    res.on('data', function (chunk) {
        context.log('-- send_line : BODY: ' + chunk);
    });
    });
    req.on('error', function(e) {
        context.log('-- send_line : problem with request: ' + e.message);
    });
    req.write(postData);
    req.end();
}

// 手紙シャッフル
function shuffle(array) {
  var n = array.length, t, i;
  while (n) {
    i = Math.floor(Math.random() * n--);
    t = array[n];
    array[n] = array[i];
    array[i] = t;
  }
  return array;
}

// 瓶詰
function push_bottles(context, event) {

    shuffled_lettersNum = shuffle(["1", "2", "3"]);

    var postData = JSON.stringify(
        {'to' : event.source.userId, 
        'messages' : [
            {
            "type": "template",
            "altText": "『瓶画像と本文へのリンク』です。",
            "template": {
                "type": "carousel",
                "columns": [
                    {
                        "thumbnailImageUrl": "https://（アカウント）.blob.core.windows.net/bins/btl_typeB.png",
                        "text": "よほど以前に漂着致したる封瓶",
                        "actions": [
                            {
                                "type": "uri",
                                "label": "瓶を開ける",
                                "uri": "https://（アカウント）.blob.core.windows.net/bins/" + shuffled_lettersNum[0] + ".jpg"
                            }
                        ]
                    },
                     {
                        "thumbnailImageUrl": "https://（アカウント）.blob.core.windows.net/bins/btl_typeA.png",
                        "text": "よほど以前に漂着致したる封瓶",
                        "actions": [
                            {
                                "type": "uri",
                                "label": "瓶を開ける",
                                "uri": "https://（アカウント）.blob.core.windows.net/bins/" + shuffled_lettersNum[1] + ".jpg"
                            }
                        ]
                    },
                    {
                        "thumbnailImageUrl": "https://（アカウント）.blob.core.windows.net/bins/btl_typeB.png",
                        "text": "よほど以前に漂着致したる封瓶",
                        "actions": [
                            {
                                "type": "uri",
                                "label": "瓶を開ける",
                                "uri": "https://（アカウント）.blob.core.windows.net/bins/" + shuffled_lettersNum[2] + ".jpg"
                            }
                        ]
                    }                    
                ]
            }
            }
            ]}
        );
    send_line(postData, context, "push");
}; 

// 浜辺
function reply_beach(context, event) {
    var postData = JSON.stringify({
    'replyToken' : event.replyToken,
    'messages' : [
        {
            "type": "image",
            "originalContentUrl": "https://（アカウント）.blob.core.windows.net/bins/beach.jpg",
            "previewImageUrl": "https://（アカウント）.blob.core.windows.net/bins/beach.jpg"
        }
    ]
    });
    send_line(postData, context, "reply");
};

function post_message(context, event) {
  if (event.type == 'message'){
    var messageType = event.message.type;
    if (messageType == 'text') {
        reply_beach(context, event);   
        push_bottles(context, event);
    } else           
        reply_beach(context, event);
    };
}

module.exports = function (context, myQueueItem) {
  context.log('------------ JavaScript queue trigger function', myQueueItem);
  myQueueItem.events.forEach(event => post_message(context, event));
  context.done();
};
```

# BOT内画像&BOT操作動画
画像は下記で

- 浜辺と瓶の画像は、安定と信頼の（瓶詰手紙画像が2種類もあって嬉しかった！） [いらすとや](http://www.irasutoya.com/) さんから
- 手紙の画像は下記方法で
 - Chrome + [Full Page Screen Capture](https://chrome.google.com/webstore/detail/full-page-screen-capture/fdpohaocaechififmbbbbbknoalclacl) で、[青空文庫 - 瓶詰地獄](http://www.aozora.gr.jp/cards/000096/files/2381_13352.html) をページ全体キャプチャ
 - [幕末古写真ジェネレータ](http://labs.wanokoto.jp/olds) + gimp　で加工

準備しました。

また、操作動画は、PC版LINEだと今回使ったカルーセルテンプレートは表示されない（下記画像）ので、
<img width="237" alt="pc.JPG" src="https://qiita-image-store.s3.amazonaws.com/0/121307/a7d3fa91-580f-b53d-97c7-beed965c111d.jpeg">

スマホアプリ版LINEの動画を撮る必要があり、

- Androidアプリ「Az Screen Recorder」

を使いました。（無償録画機能＋課金サブセット（トリミング、画面切り取り、GIF変換など、オールインワンで便利でした））



# さいごに
満足いく読書（？）体験ができました！

操作動画アニメGIF（※1アクション（ループ再生））

![sousa_loop.gif](https://qiita-image-store.s3.amazonaws.com/0/121307/78e580d4-9b7f-71e7-0db5-8e5d6e4c001e.gif)