年末年始Webアプリ開発自習の記録2: Node.jsでHabiticaにPOSTを送るWebアプリケーション作成

#はじめに
**[連休を機に考える、怠惰な私の自習戦略][Plan]**にて立てた計画に沿った自習の記録2回目です。
前回：**[年末年始Webアプリ開発自習の記録1: Node.js 開発環境準備][Prev]**



#HabiticaのAPIを調査
何を送ればHabiticaの日課を達成できるのか調べます。
**[Task - Get a user's tasks](https://habitica.com/apidoc/#api-Task-GetUserTasks)** にUserのTaskを取得する方法が書いてありますが、UserのIDとかAPI Keyとかも一緒に送るはずです。
**[Full API-TaskSchema example](http://habitica.wikia.com/wiki/Thread:16466)** にx-api-userとx-api-keyをHeaderに入れるように書いてありました。これらはHabiticaにログインした後のページで取得できました。

```
GET https://habitica.com/api/v3/tasks/user?type=dailys
```
これで日課の一覧がJson山盛りで返ってきました。

先ほど取得したJsonの、目的のタスクの"id"要素を使い、**[Task - Score a task](https://habitica.com/apidoc/#api-Task-ScoreTask)** を参考にしてPOSTを送ります

```
POST https://habitica.com/api/v3/tasks/05084be1-4f85-42a8-90bb-6314361ac128/score/up
```
返ってきたJsonの先頭に"success": true とあったので多分うまくいったでしょう。
あとはRequestを受けた後にHeader付きでこれを送る仕組みを作り、サーバー上で動かせば最低限のものはできたことになります。

#コーディング
手持ちの書籍やネットを活用し、つらつらと書いていきます。

```javascript:main.js
const http = require("http");
const https = require("https");
const url = require("url");

var server = http.createServer(onRequestFromClient);

function onRequestFromClient(req, res)
{
    var url_parts = url.parse(req.url, true);
    console.log(url_parts);

    var relayReq = https.request(options, function (relayRes)
    {
        var chunks = [];

        relayRes.on("data", function (chunk)
        {
            chunks.push(chunk);
            console.log("chunk pushed");
        });

        relayRes.on("end", function ()
        {
            var body = Buffer.concat(chunks);
            console.log(body.toString());
            //res.write(body);
            res.end();
        });
    });

    relayReq.end();
}

var options =
{
    "method": "POST",
    "hostname": "habitica.com",
    "port": null,
    "path": "/api/v3/tasks/05084be1-4f85-42a8-90bb-6314361ac128/score/up",
    "headers":
    {
        "content-length": "0",
        "x-api-key": "xxxxx",
        "x-api-user": "xxxxx",
        "content-type": "application/json"
    }
};

server.listen(3000);
console.log("Server started");
```

#実験
とりあえずRequestを受け取ったらHabiticaにRequestを送信し、そのResponseを書き出すことにした。

```
> node .\main.js
Server started
Url {
  protocol: null,
  slashes: null,
  auth: null,
  host: null,
  port: null,
  hostname: null,
  hash: null,
  search: '?msg=hello',
  query: [Object: null prototype] { msg: 'hello' },
  pathname: '/',
  path: '/?msg=hello',
  href: '/?msg=hello' }
chunk pushed
{"success":false,"error":"NotAuthorized","message":"セッションが古くなっています。ページを更新するか、syncボタンを押して下さい。"}
```
**うまくいかない。**チクショウ。
しかしありがたいことにセッションが古いと、理由が明らかにされています。
実際ChromeにインストールしたRestlet Clientからはうまくいってましたが、Habiticaからログアウトしたら同じエラーが出ました。
そしてもう一度ログインしたらうごくようになりました。
セッションの概念とかをいまいち理解していませんが、次はそのあたりを調べてトライアンドエラーをするしかないと思い、セッションやクッキーについて調べ、ログインして返ってきたクッキーを保存したりもしましたが、結果は同じでした。

#コーディングの続き
色々試してわかったことは、この実験コードに何ら問題はなく、実行する日課に1日毎の繰り返し設定がされていた場合、それは1日1回しか実行できないようになっていただけでした。かなり時間を使いましたが、セッションやクッキーについて多少知識を得られたのでよしとします。

##少し書き直したコード
main.jsからoptionsと定義していたオブジェクトをconfig.jsに切り出しました。
また、optionsに対応するidを定義し、http://URL/id となる場合に実行するように改良しました。

```json:config.json
[
    {
        "options":{
            "method": "POST",
            "hostname": "habitica.com",
            "port": null,
            "path": "/api/v3/tasks/05084be1-4f85-42a8-90bb-6314361ac128/score/up",
            "headers":
            {
                "cookie": "",
                "content-length": "0",
                "x-api-key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
                "x-api-user": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
                "content-type": "application/json"
            }
        },
        "id": "10732D14-C569-40F3-AECE-83B06BEC9BE1"
    }
]
```

```javascript:main.js
const http = require("http");
const https = require("https");
const url = require("url");
const config = require("./config.json");

var server = http.createServer((req, res) => {
    var options;
    var url_parts = url.parse(req.url, true);
    console.log(url_parts);

    config.forEach(element => {
        if (url_parts.path == "/" + element.id) {
            console.log("element found");
            options = element.options;
        }
    });
    if (options != undefined) {
        var relayReq = https.request(options, (relayRes) => {
            var chunks = [];

            relayRes.on("data", (chunk) => {
                chunks.push(chunk);
                console.log("chunk pushed");
            });

            relayRes.on("end", () => {
                var body = Buffer.concat(chunks);
                console.log(body.toString());
                if(relayRes.headers['set-cookie'] != undefined) {
                    options.headers['cookie'] =  relayRes.headers['set-cookie'];
                    console.log("Set Cookie");
                }

                res.writeHead(relayRes.statusCode, relayRes.headers)
                res.write(body.toString());
                res.end();
            });

            relayRes.on("error", (e) => {
                console.log("Error: " + e.message);
                res.write(e.message);
                res.end();
            });
        });

        console.log(options.toString());
        relayReq.end();
    }
    else {
        res.writeHead(400, {"Content-Type": "application/json"});
        res.write(JSON.stringify({
            "success": false,
            "error": "Bad Request",
            "message": "Wrong request."
        }));
        res.end();
    }
});

server.listen(3000);
console.log("Server started");
```

##気付いたこと
書き慣れないコードは書いてると、これどうなっているんだと疑問がわいてきます。

###変数のスコープは？
**[JavaScriptのスコープ総まとめ](https://qiita.com/Kazuuuuuki/items/3c40bbe6e7bb7ff84e3e)**を参考にさせていただきました。ブロックスコープがないんですね。
varで宣言するとローカルスコープになるようです。これでよく大規模開発できるなというのが率直な感想です。
**[イマドキのJavaScriptの書き方2018](https://qiita.com/shibukawa/items/19ab5c381bbb2e09d0d9)**を参考にすると納得。letというものがあるんですね。

###chuncsは同時アクセスされた時に大丈夫なの？
具体的にはこの部分のことです。

```javascript
relayRes.on("data", (chunk) => {
    chunks.push(chunk);
    console.log("chunk pushed");
});
```
POSTのデータは分割してやってくる可能性があるため、このように扱うのが基本だそうです。
一つのクライアントからのデータをこのように処理している間、他のクライアントからもこのメソッドにアクセスされたらどうなるのか？という疑問です。
多分ですけど1回のPOST毎にrelayRes自体が別の変数になるため、relayRes.onで渡すデリゲートも別ものだから大丈夫･･･ということなのでしょうか。
インスタンスを作ったりしないし、ブロックスコープがないためか、感覚がつかめません。

###コールバックをfunctionに分離するとIntelliSenseが機能しない
前回このように書いていましたが、コールバックとして渡すfunctionを余所で定義すると、変数reqとresのメソッドがIntelliSenseで補完されません。

```javascript
var server = http.createServer(onRequestFromClient);

function onRequestFromClient(req, res)
{
    var url_parts = url.parse(req.url, true);
    console.log(url_parts);
    ...
}
```
多分ですけどreqとresに型定義がないからですよね。サーバー上での動作確認後、Typescriptに手を出してみようと思います。


次はサーバーサイドの設定をして、サーバーで動かします。
次回：**[年末年始Webアプリ開発自習の記録3: Node.jsをサーバーで動作させる][Next]**

[Plan]:https://qiita.com/dmorita/items/7a85ee9c2de506c27502
[Prev]:https://qiita.com/dmorita/items/c32acea2241560cb262d
[Next]:https://qiita.com/dmorita/items/877420797cdcfb2e1ee3