20190506のNode.jsに関する記事は13件です。

「Hexo 内部探訪」のご紹介

静的サイト・ジェネレータのひとつである、Hexo
最近、ブログを書くために使い始めました。

screen-20190506-233803.png

使うというだけでなく、プラグイン機構を活かし、開発もはじめました。
hexo-tag-google-photos-album 公開しました
まだまだ荒削りな部分が残りますが、さしあたり使う分には困らない程度まではできました。

今後、プラグインの開発を進めるにあたり、方法や内部構造などを調べています。
それで、成果(?)をアウトプットしていこうと考え、まとめています。
また、それを知っていただいたり、フィードバックなどを得られたり、ということを期待して、Qiitaにも書いてみようかと思いました。

中身は私のブログに、少しずつ記事をあげてますので、ご興味あればご覧ください。(宣伝乙)

今後も連載継続予定。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

テンプレート作成

初めに

前回は生httpでサーバサイドを作成しましたが、今回はテンプレートを使いたいので
express.jsを使用します

事前準備

以下のコマンドを実行してexpress.jsをインストールする

npm install express --save

写真サイトの作成

 ゴールは写真共有サイトの作成になります
 今回は写真を表示する仕組みを作ります

サーバーサイド作成

やっている事としては、imgディレクトリ配下のjpgファイルを全て取得してテンプレートに渡す

// ①HTTPサーバを作成するための必要
const express = require("express");
let app = express();
// ディレクトリ読み込み用
let fs = require('fs');

const IMG_PATH = 'img/img/';

// テンプレートエンジンの指定
app.set("view engine", "ejs");
// 静的ファイルを読み込めるようにする
app.use(express.static('css'));
app.use(express.static('img'));


app.get("/", function(req, res) {

  let fileList = [];
  try {
    // img配下のファイルをすべて読み込む
    let files = fs.readdirSync(IMG_PATH);
    // jpgの画像を絞り込む
    files.filter(function(file) {
      return fs.statSync(IMG_PATH + file).isFile() && /.*\.jpg$/.test(file); //絞り込み
    }).forEach(function(file) {
      fileList.push(file);
    });
    let data = {
      items : fileList
    }
    console.log(data);
    // レンダリングを行う
    res.render("./index.ejs", data);
  } catch (err) {
    // 読み込めない場合エラー画面に繊維
    console.error(err);
    res.render("./error.ejs");
  }
});

// サーバ実行
app.listen(8081);

 テンプレートの作成

・index.ejs

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <title></title>
  <header class="header">
    <!-- タイトルとヘッダ画像の挿入 -->
    <h1>写真投稿サイト</h1>
    <div class="header-img"><img class="img-header" src="header/header.jpg" alt="" />

  </header>
  <!-- UIkit CSS -->

  <link rel="stylesheet" href="style.css" />
</head>

<body>
  <!-- 共有する写真のパスを渡して全部表示する -->
  <%- include("./item.ejs", items) %>
</body>
</html>

・item.ejs

<div>
<p>写真一覧</p>

<!-- 横スライドショーで表示 -->
<ul class="horizontal-list">
  <% for (let item of items) { %>
    <li class="item">
      <!-- クリック時は正規のファイルをダウンロードさせる -->
      <a href="img/" download="img/">
        <!-- 画面に表示するのは小さくした画像(容量削減のため) -->
        <img src="img_mini/s" alt="s">
      </a>
    </li>
    <% } %>
</ul>

※写真全部で2G超えています。取得時はwifi環境で実施してください。
 また、zipファイルのためダウンロード前に解凍環境を確認してください
<a href="136-231570-335954_photo_l_part1_of_2.zip" download>一括ダウンロードその1</a><br>
<a href="136-231570-335954_photo_l_part2_of_2.zip" download>一括ダウンロードその2</a><br>


・style.css


## スタイル
/** ヘッダのスタイル*/
header h1 {
  font-size: 2.0rem;
  line-height: 1.5rem;
  background-color: #7CB342;
  padding: 0px 5%;
  color: #fff;
}


.horizontal-list {
  overflow-x: auto;
  white-space: nowrap;
  -webkit-overflow-scrolling: touch;
}

.item {
  /* 横スクロール用 */
  display: inline-block;
  margin: 16px;
  background: rgba(255, 0, 0, 0.4);
}

/** ヘッダの画像だけは横いっぱいに表示 */
.img-header {
  width: 100%;
  max-width: 100%;
  height: auto;
}

完成図

完成図です。顔はマスキングしてますのであしからず

無題.png

総括

って感じで一応見せる用のサイトは作成できました。
シンプルすぎますがw
一応次はドメイン取得して外部公開方法予定です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

nginxをデフォルトport以外で起動してnode.jsのAPIを動かすまで

nodeでREST-APIを作ってnginxで動かすまでのメモ書き
以前からさくらVPSを使用しており。既にApacheが80番portを使用。
Apacheは設定を結構いじってしまっていたり、すでに複数サービス運用していることから、
この際、お試し用のWEBサーバとしてnginxを立ててみようという経緯

前提

  • port80はすでにapacheで使用している
  • node.jsのExpressを利用してREST-APIを作成
  • 環境はさくらVPS
  • 将来的には複数ドメインでの運用を検討

node.jsのアプリを起動

バックグラウンド実行。
ここら辺は任意の起動法で

$ nohup npm start &

! note
外部に公開する場合はエラー時のブラウザ表示内容に注意!
NODE_ENVがdevelopのままで起動していないか注意してください。
けっこうな情報量を晒すことになります。
対処などはこちらが参考になります

nginxの設定

nginx.conf修正

portをデフォルトの80から81に変更

/etc/nginx/nginx.conf
...
    server {
        listen       81 default_server;
        listen       [::]:81 default_server;
        server_name  _;
        root         /usr/share/nginx/html;

devt-api.conf作成

node.jsのアプリはport3000を使って起動。
外部に公開するportとして4999とする。

/etc/nginx/conf.d/devt-api.conf
upstream sampleAPP {
    server localhost:3000;
}
server {
    listen       4999;
    server_name  {{YOUR_DOMAIN}};
    index        index.php;
    access_log   /var/log/nginx/{{APP_NAME}}.access.log;
    location / {
        proxy_pass http://sampleAPP/;
    }
}

nginxの起動

自動起動設定
$ sudo systemctl enable nginx
起動
$ sudo systemctl start nginx

port 4999開放

このままだと、外部から呼び出せないため、portを開放

port4999を許可
$ firewall-cmd --zone=public --add-port=4999/tcp --permanent
設定を反映
$ firewall-cmd --reload

ブラウザからアクセス

以下のURLでアクセス可能なはず
http://{{YOUR_DOMAIN}}:4999/

SSLの設定

証明書の取得

SSL証明書(Let’s Encrypt)を導入
手順とかは省略
ここらへんがわかりやすかったです。

devt-api.confの編集

証明書と中間証明書のfullchain.pemと秘密鍵のprivkey.pemを追記してあげる

...
server {
    listen       4999 ssl;
    server_name  {{YOUR_DOMAIN}}
    ssl_certificate     /etc/letsencrypt/live/{{YOUR_DOMAIN}}/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/{{YOUR_DOMAIN}}/privkey.pem;
...

nginx再起動

再起動
$ sudo nginx -s reload

ブラウザからアクセス

httpsでのアクセス可能になるはず
https://{{YOUR_DOMAIN}}:4999/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ETHのtestnet faucetを作る with firebase cloud functions

はじめに

FirebaseのCloud Functionsを使ってNode.js(TypeScript)のETH testnet用トークン配布システムを作ります:fire:

事前準備

まず、何らかの方法でETHのアドレスと秘密鍵を用意してください
あらかじめtestnet(ここではrinkeby)のETHを入れておきましょう

コードから作成する方法

セットアップ

firebaseプロジェクトをこちらから作ってください

firebaseのCLI入れてない方は入れましょう

npm install -g firebase-tools
mkdir eth-faucet
cd eth-faucet
firebase login
firebase init

ここでFunctionsを選択
スクリーンショット 2019-05-06 14.42.37.png

その後「Select a default Firebase project for this directory:」と言われるので先ほど作ったFirebaseのプロジェクトを選択

※反映されてない場合は「don't setup a default project」を選択し、あとでfirebase use --add [project-id]とすればOK

スクリーンショット 2019-05-06 14.47.13.png

TypeScriptを選択しTSLintも導入してもらいましょう

最後にFirebase Consoleでプロジェクトの課金設定をBlazeにします

理由は無料版だとfirebase関連以外の外部パッケージが使えないからです

スクリーンショット 2019-05-06 14.55.08.png

スクリーンショット 2019-05-06 14.55.21.png

作っていく

Express, web3, ethreumjs-txを導入します
ExpressはNode.jsの最小限なフレームワークです
Expressの型定義ファイルもインストールします

cd functions
npm install --save express web3 ethereumjs-tx
npm install --save-dev @types/express

次にsrc/index.tsを編集します
全体のコードはこちらです

src/index.ts
import * as functions from 'firebase-functions';
import * as Express from 'express';

const Web3 = require('web3');
const Tx = require('ethereumjs-tx');

const app = Express();

const networks  = {
  main: 'wss://mainnet.infura.io/ws/v3/',
  ropsten: 'wss://ropsten.infura.io/ws/v3/',
  kovan: 'wss://kovan.infura.io/ws/v3/',
  rinkeby: 'wss://rinkeby.infura.io/ws/v3/'
};

// InfuraのAPI-KEY(作る)
const apiKey = 'YOUR-API-KEY';
// テストネット(ここではrinkebyを選択)
const network = networks.rinkeby;

// rinkebyネットワークに接続する処理
const getWeb3 = () => {
  const provider = network + apiKey;
  const web3 = new Web3(provider);
  return web3;
}

app.get('/:address', async(req:Express.Request, res: Express.Response) => {
    try {
        const from = '用意したETHアカウントのアドレス';
        const to = req.params.address;
        const privateKey = '用意したETHアカウントの秘密鍵';
        const amount = 0.01  // faucetとして配布したいETHの量
        const web3 = getWeb3();

        const value = web3.utils.toWei(amount.toString(), 'ether');

        const gasParams = {
            from: from,
            to: to,
            value: value,
        }
        const gasLimit = await web3.eth.estimateGas(gasParams);
        const gasPrice = await web3.eth.getGasPrice();
        const count = await web3.eth.getTransactionCount(from);

        const data = null;
        const id = await web3.eth.net.getId();


        const params = {
            nonce: web3.utils.numberToHex(count),
            gasPrice: web3.utils.numberToHex(gasPrice),
            gasLimit: web3.utils.numberToHex(gasLimit),
            to: to,
            from: from,
            value: web3.utils.numberToHex(value),
            data: data,
            chainId: id
        }
        //Transaction作成
        const tx = new Tx(params);
        const _privateKey = Buffer.from(privateKey.slice(2), 'hex');

        //Transactionに秘密鍵で署名
        tx.sign(_privateKey);
        const rawTx = '0x' + tx.serialize().toString('hex');
        const result = await web3.eth.sendSignedTransaction(rawTx);

        //Transactionを送信した結果がtrueならresponseを返す
        if(result.status == true) {
            res.send('送金が完了しました\n')
        }
    }
    catch(error) {
        res.status(500).send(error)
    }
})

exports.app = functions.https.onRequest(app);

その後以下を実行

firebase deploy --only functions:app

そしてcurlでルートにETHの送り先アドレスを込めて叩く

curl https://us-central1-<project-id>.cloudfunctions.net/app/<送り先のアドレス>

コンソール上で送金が完了しましたと出ればOK

この時、Error: could not handle the requestのようにtimeout errorが出ることがありますがEtherscanでみるときちんと送金はされています

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

今更ながらSlackBotを作ってみた:Incoming Webhooks

SlackBotを作ってきましたが、これがいよいよ最後です。
 今更ながらSlackBotを作ってみた
 今更ながらSlackBotを作ってみた:Slach Commands
 今更ながらSlackBotを作ってみた:Interactive Components
 今更ながらSlackBotを作ってみた:ダイアログ

今回はIncomming Webhooksです。
いつでも好きな時に、特定のチャネルや特定のユーザにメッセージを送信します。

以下が参考になります。

Incoming Webhooks
 https://api.slack.com/incoming-webhooks

Incoming Webhooksを有効にする

いつもの、Setting-Basic Informationを開きます。

image.png

Incoming Webhooksを選択します。

image.png

Activate Incoming WebhooksをOnにします。

image.png

次に、下の方にある「Add New Webhhooks to Workspace」リンクをクリックします。

image.png

投稿先として、メッセージを送信したいチャネルまたはユーザ名を選択し、許可するボタンを押下します。
例えば、最初に作ったチャネル#testprojectを選択しました。

image.png

そうすると、Webhook URLのテーブルに先ほどのチャネルとURLが追加されているのがわかります。Webhook URLは後で使うので覚えておきます。

サーバ側の実装

Incoming Webhookによるメッセージ送信するきっかけとして、前回の投稿で作ったアンケートへの回答があった時としたいと思います。

app.submissionは変更、それ以外は追加です。

controllers\slack_testbot\index.js
const WEBHOOK_URL_INCOMING = Webhook URL;


app.submission(async (body, web) =>{
    var message = {
        "text": '回答ありがとうございました。',
    };
    app.responseMessage(body.response_url, message );

    app.responseMessage(WEBHOOK_URL_INCOMING, { text: body.user.name + 'さんがアンケートに回答しました。' });
});

【Webhook URL】の部分をさきほど生成されたIncoming WebhookのWebhook URLに置き換えます。

動作確認

今回も、AndroidのSlackアプリで動作確認しました。

こんな感じで入力して、送信すると、

image.png

レスポンスメッセージを受信しました。

image.png

ここまでは以前と同じですが、チャネル#testprojectにもメッセージが届いています。

image.png

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

今更ながらSlackBotを作ってみた:ダイアログ

前回からの続きで、SlackBotをカスタマイズしていきます。
 今更ながらSlackBotを作ってみた
 今更ながらSlackBotを作ってみた:Slach Commands
 今更ながらSlackBotを作ってみた:Interactive Components

今回はダイアログです。
何かの申請書の入力だったり、アンケート回答がわかりやすいかもしれません。

詳細は以下を参照してください。

slack api: Interacting with users through dialogs
 https://api.slack.com/dialogs

Permission設定

前回までを進めていただいている場合は、特に追加の権限付与は不要です。

サーバ側の実装

の前に、ダイアログを表示するきっかけとして、SlashCommandsを設定します。
(ダイアログ表示の際には、trigger_idが必要です。SlashCommandsからのメッセージにはそれが含まれています)

Features-Slash Commandsを開きます。

image.png

Create New Commandボタンを押下して、作成します。

image.png

サーバ側の実装です。
app.commandは変更、それ以外は追加です。

controllers\slack_testbot\index.js
app.command(async (body, web) =>{
    if(body.command == '/hi'){
        var hour = new Date().getHours();

        var greeting = 'こんにちは';
        if( 5 <= hour && hour <= 9 )
            greeting = 'おはよう';
        else if( 18 <= hour && hour < 5 )
            greeting = 'こんにちは';

        var message = {
            text: greeting + '!' + (body.text ? (' ' + body.text + " です。") : '')
        };
        app.responseMessage(body.response_url, message );
    }else if( body.command == '/query'){
        var message = {
            text: "選択肢を表示します。",
            blocks: blocks,
        };
        app.responseMessage(body.response_url, message );
    }else if( body.command == '/dialog'){
        options.trigger_id = body.trigger_id;
        app.dialogOpen(options);
    }
});

app.submission(async (body, web) =>{
    var message = {
        "text": '回答ありがとうございました。',
    };
    app.responseMessage(body.response_url, message );
});

app.cancellation(async (body, web) =>{
    var message = {
        "text": 'キャンセルされました。',
    };
    app.responseMessage(body.response_url, message );
});

var options = {
    dialog: {
      callback_id: "dialog1",
      title: "試食のアンケート",
      submit_label: "送信する",
      notify_on_cancel: true,
      elements: [{
          type: "text",
          label: "試食した食べ物",
          name: "name",
          placeholder: 'food'
        },
        {
          label: "評価",
          type: "select",
          name: "review",
          options: [{
              label: "すごく美味しい",
              value: "very_good"
            },
            {
              label: "美味しい",
              value: "good"
            },
            {
              label: "普通",
              value: "normal"
            },
            {
              label: "不味い",
              value: "bad"
            },
            {
              label: "すごく不味い",
              value: "very_bad"
            }
          ]
        },
        {
          type: "textarea",
          label: "その他",
          name: "others",
          hint: "好きに書いてください",
          optional: "true"
        }
      ]
    },
    trigger_id: ''
};

app.commandにおいて、受信したメッセージに含まれるtrigger_idを利用しています。

動作確認

今回も、AndroidのSlackアプリで動作確認しました。

/dialogを入力すると、

image.png

ダイアログが表示されました!

image.png

こんな感じで入力して、右上の送信するを押下すると、

image.png

app.submissionが呼ばれ、レスポンスメッセージが返されました。

image.png

サーバ側では、app.submissionのコールバック関数に、以下のデータが返ってきています。

  "submission": {
    "name": "ハンバーグ",
    "review": "very_good",
    "others": null
  },

ちなみに、送信せずに、戻る をすると、以下のようにapp.cancelleationが呼ばれ、レスポンスメッセージが返ってきます。

image.png

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

今更ながらSlackBotを作ってみた:Interactive Components

前回、前々回に引き続き、SlackBotをカスタマイズしていきます。
 今更ながらSlackBotを作ってみた
 今更ながらSlackBotを作ってみた:Slach Commands

今回は、Interactive Componentsです。
絵文字や文字装飾でもある程度表現できますが、やはりボタンやセレクトや画像を使った方がより直感的にやりとりできます。

前々回の環境をそのまま使います。まだの方はぜひご参照ください。

(参考情報)
Making messages interactive
 https://api.slack.com/interactive-messages

Interactive Componentsを有効化する

いつものslack apiのSettings-Basic Informationから行います。

image.png

Interactive Componentsを選択し、InteractivityをOnにします。

image.png

入力欄「Request URL」に立ち上げたサーバのURLをフルパスで入力します。「/slack-testbot-cmd」の方です。最後にSave Changesボタンを押下します。

image.png

これで、表現力豊かなUIからの通知を受けることができました。

UIを作成する

UIを配置していきたいのですが、便利なツールがあります。
GUIで配置結果を見ながら作れるので便利です。(少々使いにくいですが)

Block Kit Builder
 https://api.slack.com/tools/block-kit-builder

image.png

以下のようなUIを作成しました。

image.png

[
    {
        "type": "actions",
        "elements": [
            {
                "type": "static_select",
                "placeholder": {
                    "type": "plain_text",
                    "text": "Select an item"
                },
                "action_id": "select1",
                "options": [
                    {
                        "text": {
                            "type": "plain_text",
                            "text": "アイテム1"
                        },
                        "value": "item1"
                    },
                    {
                        "text": {
                            "type": "plain_text",
                            "text": "アイテム2"
                        },
                        "value": "item2"
                    },
                    {
                        "text": {
                            "type": "plain_text",
                            "text": "アイテム3"
                        },
                        "value": "item3"
                    }
                ]
            }
        ]
    },
    {
        "type": "actions",
        "elements":[
            {
              "type": "button",
              "text": {
                "type": "plain_text",
                "text": "ボタン1"
              },
              "action_id": "button1"
            },
            {
              "type": "button",
              "text": {
                "type": "plain_text",
                "text": "ボタン2"
              },
              "action_id": "button2"
            }
        ]
    }
]

サーバ側の実装

その前に、Interactive ComponentsのUIを表示させるトリガーを用意します。
なんでもよいのですが、もう一つSlashCommandsを作って、それを呼び出すとUIを表示するようにしたいと思います。

例えば、以下のように、「/query」というコマンドを作ってみましょう

image.png

それではさっそく、サーバ側の実装です。
app.commandは変更で、それ以外は追加です。

controllers\slack_testbot\index.js
app.command(async (body, web) =>{
    if(body.command == '/hi'){
        var hour = new Date().getHours();

        var greeting = 'こんにちは';
        if( 5 <= hour && hour <= 9 )
            greeting = 'おはよう';
        else if( 18 <= hour && hour < 5 )
            greeting = 'こんにちは';

        var message = {
            text: greeting + '!' + (body.text ? (' ' + body.text + " です。") : '')
        };
        app.responseMessage(body.response_url, message );
    }else if( body.command == '/query'){
        var message = {
            text: "選択肢を表示します。",
            blocks: blocks,
        };
        app.responseMessage(body.response_url, message );
    }
});

app.action(async (body, web) =>{
    var text = '';
    for( var i = 0 ; i < body.actions.length ; i++ ){
        var action = body.actions[i];
        text += action.action_id;
        if( action.type == 'static_select')
            text += 'の' + action.selected_option.value;
        text += 'が選択されました。\n';
    }
    var message = {
        text: text
    };
    app.responseMessage(body.response_url, message );
});

var blocks = [
    {
        "type": "actions",
        "elements": [
            {
                "type": "static_select",
                "placeholder": {
                    "type": "plain_text",
                    "text": "Select an item"
                },
                "action_id": "select1",
                "options": [
                    {
                        "text": {
                            "type": "plain_text",
                            "text": "アイテム1"
                        },
                        "value": "item1"
                    },
                    {
                        "text": {
                            "type": "plain_text",
                            "text": "アイテム2"
                        },
                        "value": "item2"
                    },
                    {
                        "text": {
                            "type": "plain_text",
                            "text": "アイテム3"
                        },
                        "value": "item3"
                    }
                ]
            }
        ]
    },
    {
        "type": "actions",
        "elements":[
            {
              "type": "button",
              "text": {
                "type": "plain_text",
                "text": "ボタン1"
              },
              "action_id": "button1"
            },
            {
              "type": "button",
              "text": {
                "type": "plain_text",
                "text": "ボタン2"
              },
              "action_id": "button2"
            }
        ]
    }
];

app.commandにおいて、/queryが来たら、作成したUIを返すようにしています。
そして、ユーザによってUIの部品を選択すると、都度app.actionが呼び出されます。選択された部品名をレスポンスメッセージにして返しています。

ちなみに、今回のUIの場合は、UIを選択されたとき、以下のような応答がSlackから返ってきています。

選択肢が選択された場合

  "actions": [{
    "action_id": "select1",
    "block_id": "hTeDk",
    "selected_option": {
      "text": {
        "type": "plain_text",
        "text": "アイテム2",
        "emoji": true
      },
      "value": "item2"
    },
    "type": "static_select",
    "action_ts": "1557116165.623264"
  }]

ボタンがクリックされた場合

  "actions": [{
    "action_id": "button2",
    "block_id": "KEM",
    "text": {
      "type": "plain_text",
      "text": "ボタン2",
      "emoji": true
    },
    "type": "button",
    "value": "button2",
    "action_ts": "1557116194.565853"
  }]

動作確認

AndroidのSlackアプリから操作してみました。

/queryコマンドを入力すると、

image.png

UIが表示されました。
解像度が小さいスマホで表示させたので、解像度が大きい場合は見え方が違うと思います。

image.png

選択肢を選択すると

image.png

という感じに、レスポンスメッセージが返ってきました。

補足

UIの各部品が選択されるごとに、メッセージが通知されました。
次回は、ダイアログを紹介します。都度通知ではなくすべての入力完了時に通知されるようになります。

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ETHのアカウントを作る

概要

Ethreumのアカウントをweb3.jsを使って作成します

作り方

mkdir create-eth-account
cd create-eth-account
touch create.js

次にweb3をインストールします

npm i --save web3

あとはcreate.jsファイルを以下のように編集します

create.js
const Web3 = require('web3');

const networks  = {
    main: 'wss://mainnet.infura.io/ws/v3/',
    ropsten: 'wss://ropsten.infura.io/ws/v3/',
    kovan: 'wss://kovan.infura.io/ws/v3/',
    rinkeby: 'wss://rinkeby.infura.io/ws/v3/'
};

//InfuraのAPI-KEY
const apiKey = 'YOUR-API-KEY';

//ここではどのネットワークでも良い
const network = networks.rinkeby;

const getWeb3 = () => {
    const provider = network + apiKey;
    const web3 = new Web3(provider);
    return web3;
}

const createAccount = async() => {
    const web3 = getWeb3();
    const account = web3.eth.accounts.create();
    console.log(account);
}

createAccount();

あとは以下を実行すればアカウントが作れます

node create.js

こんな感じで表示されます

スクリーンショット 2019-05-06 13.54.27.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

今更ながらSlackBotを作ってみた:Slach Commands

前回に引き続き、SlackBotをカスタマイズしていきます。
 今更ながらSlackBotを作ってみた

今回は、スラッシュコマンドです。
Slackには、/で始まるコマンドがたくさん用意されていますよね。
そこに、独自のコマンドを付け加えることができ、それをSlackBotが処理できるようになります。

前回の環境をそのまま使います。まだの方はぜひご参照ください。

SlashCommandsを作成する

Setting-Basic Information のページを表示し、Slash Commandsを選択します。

image.png

それでは、「Create New Command」ボタンを押下しましょう。

image.png

「hi」という呼びかけに対して、あいさつ文と一緒にエコーバックするコマンドを作ってみましょう。例えば、以下のような感じで作成してみました。

・Command:/hi
・Request URL : 立ち上げたサーバのエンドポイントをフルパスで指定します。/testbot-cmdの方です。
・Short Description:やあ
・Usage Hint:呼びかけに応答します。

image.png

「Save」ボタンを押下します。
インストール済みのアプリを更新する必要がありますので、click hereのリンクをクリックします。あるいは、Setting-Install Appを選択して、「Reinstall App」ボタンを押下します。

image.png

すると、以下のような確認ページが表示されるので、「許可する」ボタンを押下します。

image.png

これで、Slack側の準備ができました。

サーバ側の準備

次は、サーバ側を準備します。

すでに実装してあるコードに以下を追記します。

controllers\slack_testbot\index.js
//・・・
app.command(async (body, web) =>{
    if(body.command === '/hi'){
        var hour = new Date().getHours();

        var greeting = 'こんにちは';
        if( 5 <= hour && hour <= 9 )
            greeting = 'おはよう';
        else if( 18 <= hour && hour < 5 )
            greeting = 'こんにちは';

        var message = {
            text: greeting + '!' + (body.text ? (' ' + body.text + " です。") : '')
        };
        app.responseMessage(body.response_url, message );
    }
});
//・・・

app.commandに関数を登録しておくと、コマンドが入力されたときにコールバックされてきます。

レスポンスメッセージを返す方法として、以下の3種類があります。

・Confirm your receipt of the payload. : 受信できたことを知らせるのみ
・Do something useful in response right away. : すぐにレスポンスメッセージを返す。
・Do something useful in response later. : 後でレスポンスメッセージを返す。

slack-utils.jsでは、「後でレスポンスを返す」 方法をとっています。
body.response_urlに後で返す先のURLがあります。それを使って、app.responseMessageを呼び出してメッセージを返しています。

詳細は以下を参考にしてください。

slack api:Slash Commands
https://api.slack.com/slash-commands

レスポンスメッセージの詳細は、前回同様以下を参考にしてください。

slack api:Reference Message payloads
https://api.slack.com/reference/messaging/payload

動作確認

こんな感じでできました。

入力中。

image.png

応答を受信。

image.png

SlackBot用にユーティリティslack-utils.jsを用意しておいたので、わりと簡単に実装できるようになったのではないかと思います。

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

今更ながらSlackBotを作ってみた:Slash Commands

前回に引き続き、SlackBotをカスタマイズしていきます。
 今更ながらSlackBotを作ってみた

今回は、スラッシュコマンドです。
Slackには、/で始まるコマンドがたくさん用意されていますよね。
そこに、独自のコマンドを付け加えることができ、それをSlackBotが処理できるようになります。

前回の環境をそのまま使います。まだの方はぜひご参照ください。

(参考情報)
slack api:Slash Commands
 https://api.slack.com/slash-commands

SlashCommandsを作成する

Setting-Basic Information のページを表示し、Slash Commandsを選択します。

image.png

それでは、「Create New Command」ボタンを押下しましょう。

image.png

「hi」という呼びかけに対して、あいさつ文と一緒にエコーバックするコマンドを作ってみましょう。例えば、以下のような感じで作成してみました。

・Command:/hi
・Request URL : 立ち上げたサーバのエンドポイントをフルパスで指定します。/testbot-cmdの方です。
・Short Description:やあ
・Usage Hint:呼びかけに応答します。

image.png

「Save」ボタンを押下します。
インストール済みのアプリを更新する必要がありますので、click hereのリンクをクリックします。あるいは、Setting-Install Appを選択して、「Reinstall App」ボタンを押下します。

image.png

すると、以下のような確認ページが表示されるので、「許可する」ボタンを押下します。

image.png

これで、Slack側の準備ができました。

サーバ側の準備

次は、サーバ側を準備します。

すでに実装してあるコードに以下を追記します。

controllers\slack_testbot\index.js
//・・・
app.command(async (body, web) =>{
    if(body.command === '/hi'){
        var hour = new Date().getHours();

        var greeting = 'こんにちは';
        if( 5 <= hour && hour <= 9 )
            greeting = 'おはよう';
        else if( 18 <= hour && hour < 5 )
            greeting = 'こんにちは';

        var message = {
            text: greeting + '!' + (body.text ? (' ' + body.text + " です。") : '')
        };
        app.responseMessage(body.response_url, message );
    }
});
//・・・

app.commandに関数を登録しておくと、コマンドが入力されたときにコールバックされてきます。

レスポンスメッセージを返す方法として、以下の3種類があります。

・Confirm your receipt of the payload. : 受信できたことを知らせるのみ
・Do something useful in response right away. : すぐにレスポンスメッセージを返す。
・Do something useful in response later. : 後でレスポンスメッセージを返す。

slack-utils.jsでは、「後でレスポンスを返す」 方法をとっています。
body.response_urlに後で返す先のURLがあります。それを使って、app.responseMessageを呼び出してメッセージを返しています。

レスポンスメッセージの詳細は、前回同様以下を参考にしてください。

slack api:Reference Message payloads
https://api.slack.com/reference/messaging/payload

動作確認

こんな感じでできました。

入力中。

image.png

応答を受信。

image.png

SlackBot用にユーティリティslack-utils.jsを用意しておいたので、わりと簡単に実装できるようになったのではないかと思います。

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Angularでtarの脆弱性(Arbitrary File Overwrite)を指摘されたので修正する

概要

この記事ではAngularの新規プロジェクトを作成した所、tarの脆弱性(Arbitrary File Overwrite)を指摘されたので、その修正方法について説明します。

angular/cli のバージョン

$ng version

     _                      _                 ____ _     ___
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/


Angular CLI: 7.3.8
Node: 10.8.0
OS: win32 x64
Angular: 7.2.14
... animations, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router

Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.13.8
@angular-devkit/build-angular     0.13.8
@angular-devkit/build-optimizer   0.13.8
@angular-devkit/build-webpack     0.13.8
@angular-devkit/core              7.3.8
@angular-devkit/schematics        7.3.8
@angular/cli                      7.3.8
@ngtools/webpack                  7.3.8
@schematics/angular               7.3.8
@schematics/update                0.13.8
rxjs                              6.3.3
typescript                        3.2.4
webpack                           4.29.0

Angularで新規プロジェクトを作成すると脆弱性が指摘される

新規プロジェクト作成

$ng new hoge
? Would you like to add Angular routing? No
? Which stylesheet format would you like to use? CSS

脆弱性が指摘される

found 1 high severity vulnerability

脆弱性の詳細を確認

npm audit で脆弱性の詳細を確認します。npm audit はインストールしたパッケージに対してセキュリティチェックを行い、脆弱性がある場合は詳細なレポートを表示してくれます。

$npm audit

                       === npm audit security report ===                        


                                 Manual Review                                  
             Some vulnerabilities require your attention to resolve             

          Visit https://go.npm.me/audit-guide for additional guidance           


  High            Arbitrary File Overwrite                                      

  Package         tar                                                           

  Patched in      >=4.4.2                                                       

  Dependency of   @angular-devkit/build-angular [dev]                           

  Path            @angular-devkit/build-angular > node-sass > node-gyp > tar

  More info       https://nodesecurity.io/advisories/803

found 1 high severity vulnerability in 42611 scanned packages
  1 vulnerability requires manual review. See the full report for details.

4.4.2より前のバージョンを使用していると任意のファイルに上書きされる脆弱性がtarにありました。tarのバージョンを上げて対応します。

node-gyp の package.json を開く

node_modules\node-gyp\package.json

tarのバージョンを上げる

変更前

"dependencies": {
  "tar": "^2.0.0"
},

変更後

"dependencies": {
  "tar": "^4.4.8"
},

パッケージを再度インストール

$npm install

パッケージの脆弱性を自動修復

npm audit fix はインストールしたパッケージの脆弱性を自動で修復してくれます。
今回の脆弱性の場合はnode-gypのpackage.jsonを手動で修正してから実行する必要があります。

$npm audit fix

脆弱性の確認

再度、セキュリティチェックを行うと脆弱性が解消されていることが確認できます。

$npm audit

                       === npm audit security report ===                        

found 0 vulnerabilities
 in 42604 scanned packages

参考

今回はこちらのstackoverflowを参考にして対応しました。
https://stackoverflow.com/questions/55635378/npm-audit-arbitrary-file-overwrite

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Express v4 と log4js v4 で実践的なロガーを実装

はじめに

node.jsでアプリケーションログを出力する際に、業務でありがちなログのローテーションや、ログレベル毎にファイルを分けるとかの設定をlog4jsで実装してみました。

実装時の環境

以下の環境で実装しました。

・OS:Mac OS X v10.14.4
・Node:v12.1.0
・npm:v6.9.0

log4jsで指定できるログの種別

log4jsでは以下のログレベルを指定できます。

ログレベル 詳細
OFF 全てのログを取得しない。(使うことがあるのか疑問です)
MARK エラーではないが常に表示しておきたい情報を出力。(使ったことないので憶測です)
FATAL 致命的なエラーの情報を出力。(システムの動作に影響を与えるエラー)
ERROR 予期しない実行時エラーの情報を出力。
WARN 使用が終了となった機能の使用、APIの不適切な使用等に対する警告を出力。(エラーではないが正常とは言い難いものはこちらに分類されます)
INFO 通常のログ。サーバーの起動、停止、ユーザーの操作情報等を出力。
DEBUG デバッグ用のログレベル。システムの動作状況に関する詳細な情報を出力したい時に使用。
TRACE トレース用のログレベル。デバッグでは出力しきれない詳細な情報を出力。
ALL 全てのログを取得

簡単なコードで実装してみる

ひとまず、簡単に出力するしてみます。
Expressとlog4jsをインストールします。

# package.jsonを生成
$ npm init -y

# Expressインストール(実装時の最新版です)
$ npm install express@4.16.4

# log4jsインストール(実装時の最新版です)
$ npm install log4js@4.1.0

以下のようなコードでログを出力できます。

const express = require ('express');
const app = express ();

const log4js = require ('log4js');
const logger = log4js.getLogger ();
logger.level = 'debug';

app.get ('/', (req, res) => {
  logger.debug ('デバッグログが出力されます');
  res.send ('log test');
});

app.listen (3000);

出力結果は以下の通り。
このログの出力では、console.log()と変わらないですね。

[2019-05-05T19:44:34.037] [Level { level: 10000, levelStr: 'DEBUG', colour: 'cyan' }] default - デバッグログが出力されます

実践的なログ出力

業務で使用するログの出力方式としては以下の基準は満たしておきたいです。

・ログはファイルに出力する。
・ログの種別毎に分ける。(システムログ、アプリケーションログ、アクセスログ)
・ログローテーションを行う。 (30日分保持とか5MB毎にファイル生成等)

ログファイルの出力設定について

ログをファイルに出力するには、log4x系によくあるappnedersとcategoriesを使用します。

以下はnodejsで実装する場合の一例です。
appendersには、logのタイプを指定します。下の例では、console出力用のAppenderを設定しています。
categoriesには、appendersで設定したAppenderを指定して、どのレベルで出力を行うかを指定します。

// <APP ROOT>/config/log4js-config.js
const log4js = require('log4js');

// ログ出力設定
log4js.configure({
  appenders: {
    consoleLog: {
      type: "console"
    }
  },
  categories: {
    "default": {
      // appendersで設定した名称を指定する
      // levelは出力対象とするものを設定ここではALL(すべて)
      appenders: ["consoleLog"],
      level: "ALL"
    }
  }
});

// ログカテゴリはdefaultを指定する
const logger = log4js.getLogger("default");

// infoとかerrorとかで出力
logger.error("error message");

Appenderに設定するtypeでよく利用するものは以下の通り。

タイプ 詳細
console コンソールログを出力する。
file ファイルサイズによるローテーションと何ファイル保持するかを設定できる。
multiFile 主にAPIルート毎などログファイルの分離を行いたい時に使用する。
dateFile 日時でログローテーションするログの管理に使用する。何日分保持するかを設定できる。

上のコードを実行すると以下のような出力となります。
categoriesに設定したdefaultというカテゴリでログの内容が出力されます。

[2019-05-05T20:30:24.433] [Level { level: 40000, levelStr: 'ERROR', colour: 'red' }] default - error message

モジュールの設計

システムログ、アプリケーションログ、アクセスログの設計は以下の通りです。

.
├── config
│   └── log4js-config.js  ←ログ設定モジュール
├── lib
│   └── log
│       ├── logger.js        ←ルートロガーモジュール(各設定モジュールを統合する)
│       ├── accessLogger.js  ←アクセスログ出力モジュール
│       └── systemLogger.js ←システムログ出力モジュール
├── log
│   ├── access               ←アクセスログが出力されるディレクトリ
│   ├── application          ←アプリケーションログが出力されるディレクトリ
│   └── system               ←システムログが出力されるディレクトリ
├── package-lock.json
├── package.json
└── server.js                ←Expressアプリケーション

システムログの実装

まずはシステムログから実装します。
今回作成するシステムログの要件は以下の通りです。

・エラーログのみを出力。
・1ファイルが一定のサイズになった時にローテーションする。(5MBでローテーション)
・/log/system/system.log に出力する。
・世代管理は5ファイルまでとする。

File Appenderを使う

システムログの出力設定には、File Appenderを使用します。
File Appenderで指定できる設定は以下の通りです。

設定 詳細
type string 設定できる値は file のみ。
filename string 出力ファイルのパスを設定。
maxLogSize number ログファイルの最大サイズ(byteで指定)この数値に達するとローテーションが実行されます。
backups number ログファイルの最大保持数を指定。
layout object ログ出力のレイアウトを指定。(日付のフォーマット等の設定)
compress boolean ログファイルの保持数が上限に達した場合に古いログを削除するかの設定(trueに設定すると削除)

実装

ファイルを出力する際に、パスの指定をする必要があるので、pathモジュールをインストールします。

$ npm install path

ログ設定モジュールにシステムログ用の設定を追加します。

// <APP ROOT>/config/log4js-config.js
const path = require("path");
// ログ出力先は、サーバー内の絶対パスを動的に取得して出力先を設定したい
const APP_ROOT = path.join(__dirname, "../");

// ログ出力設定
// log4jsはルートロガーで使用するので、エクスポートに変更
module.exports = {
  appenders: {
    consoleLog: {
      type: "console"
    },
    // ADD
    systemLog: {
      type: "file",
      filename: path.join(APP_ROOT, "./log/system/system.log"),
      maxLogSize: 5000000, // 5MB
      backups: 5, // 世代管理は5ファイルまで、古いやつgzで圧縮されていく
      compress: true
    }
  },
  categories: {
    default: {
      // appendersで設定した名称を指定する
      // levelは出力対象とするものを設定ここではALL(すべて)
      appenders: ["consoleLog"],
      level: "ALL"
    },
    // ADD
    system: {
      appenders: ["systemLog"],
      level: "ERROR"
    }
  }
};

ルートロガーモジュールを作成します。
このモジュールは、ログ設定モジュールを元にロガーを生成します。

// <APP ROOT>/lib/log/logger.js
const log4js = require("log4js");
const config = require("../../config/log4js-config.js");
log4js.configure(config);

// それぞれのログ種別ごとに作成
const console = log4js.getLogger();
const system = log4js.getLogger("system");  // 上で作成したcategoriesのsystemで作成します。

// ログ種別のエクスポート
module.exports = {
  console,
  system
};

ログ出力モジュールは、上で作成したモジュールとは別に作成します。

// <APP ROOT>/lib/log/systemLogger.js
const logger = require("./logger").system;

module.exports = (options) => (err, req, res, next) => {
  logger.error(err.message);
  next(err);
}

Expressアプリに実装します。

// <APP ROOT>/server.js
const express = require("express");
const app = express();

app.get('/', (req, res) => {
  logger.debug('デバッグログが出力されます');
  res.send('log test');
});

// 意図的にエラーを起こすルート
app.get("/error", (req, res) => {
  throw new Error("システムログの出力テスト Errorです");
});

// systemロガー
const systemLogger = require("./lib/log/systemLogger");
// ロガーをExpressに実装
app.use(systemLogger());

app.listen(3000);

ログ出力をテストしてみます。

# サーバー起動
$ node server.js

# curlでerrorルートにアクセス
$ curl localhost:3000/error

curlでアクセスすると、以下のようなログが出力されます。
また、systemディレクトリ、system.logファイルが存在しない場合は自動生成されます。

# <APP ROOT>/log/system/system.log
[2019-05-05T21:53:57.674] [Level { level: 40000, levelStr: 'ERROR', colour: 'red' }] system - システムログの出力テスト Errorです

次は、ログローテーションの機能を確認してみましょう。
以下のコマンドを実行して5MBのファイルを生成しておきます。

$ mkfile 5m ./log/system/system.log

ログ出力をテストしてみます。

# サーバー起動
$ node server.js

# curlでerrorルートにアクセス
$ curl localhost:3000/error

system.log(5MB)のファイル名がsystem.log.1に変更され、新しくsystem.logファイルが生成されるはずです。

次は、ログの保持は5ファイルまでにしていたので、こちらを確認してみましょう。
system.log.1~5までの5ファイル(5MB)、system.log(5MB)を用意します。

ログ出力のテストをしてみます。

# サーバー起動
$ node server.js

# curlでerrorルートにアクセス
$ curl localhost:3000/error

一番古いログ(作成日付が古いもの)がgzで生成され、system.logがsystem.log.1へと名前が変更されるはずです。
以上でシステムログの実装は完了です。

アプリケーションログの実装

アプリケーションログの要件は以下の通りです。

・エラーログのみ出力。
・機能毎にログファイルを分割する。
・1ファイルが一定のサイズになった時にローテーションする。(5MBでローテーション)
・/log/application/.log に出力する。
・世代管理は5ファイルまでとする。

Multi File Appenderを使う

アプリケーションログの実装には Multi File Appenderを使用します。
Multi File Appenderで指定できる設定は以下の通りです。

設定 詳細
type string 設定できる値は multiFile のみ。
base string 出力ファイルのパスを設定。(ファイル名は記述しない)
property string ログを分離する条件を設定。
extension string ログファイル名のサフィックスを設定。(拡張子のこと)
layout object ログ出力のレイアウトを指定。(日付のフォーマット等の設定)
maxLogSize number ログファイルの最大サイズ(byteで指定)この数値に達するとローテーションが実行されます。
backups number ログファイルの最大保持数を指定。
compress boolean ログファイルの保持数が上限に達した場合に古いログを削除するかの設定(trueに設定すると削除)

実装

ログ設定モジュールにシステムログ用の設定を追加します。

// <APP ROOT>/config/log4js-config.js
const path = require('path');
// ログ出力先は、サーバー内の絶対パスを動的に取得して出力先を設定したい
const APP_ROOT = path.join(__dirname, '../');

// ログ出力設定
module.exports = {
  appenders: {
    consoleLog: {
      type: 'console',
    },
    systemLog: {
      type: 'file',
      filename: path.join(APP_ROOT, './log/system/system.log'),
      maxLogSize: 5000000,
      backups: 5,
      compress: true,
    },
    // ADD
    applicationLog: {
      type: "multiFile",
      base: path.join(APP_ROOT, "./log/application/"),
      property: "key",
      extension: ".log", // ファイルの拡張子はlogとする
      maxLogSize: 5000000, // 5MB
      backups: 5, // 世代管理は5ファイルまで、古いやつからgzで圧縮されていく
      compress: true,
    },
  },
  categories: {
    default: {
      appenders: ["consoleLog"],
      level: "ALL"
    },
    system: {
      appenders: ["systemLog"],
      level: "ERROR"
    },
    // ADD
    application: {
      appenders: ["applicationLog"],
      level: "ERROR"
    }
  },
};

ルートロガーモジュールに追加します。

// <APP ROOT>/lib/log/logger.js
const log4js = require("log4js");
const config = require("../../config/log4js-config.js");
log4js.configure(config);

// それぞれのログ種別ごとに作成
const console = log4js.getLogger();
const system = log4js.getLogger("system");
const application = log4js.getLogger("application");  // ADD

// ログ種別のエクスポート
module.exports = {
  console,
  system,
  application,  // ADD
};

Expressアプリに実装します。

// <APP ROOT>/server.js
const express = require("express");
const app = express();

app.get('/', (req, res) => {
  logger.debug('デバッグログが出力されます');
  res.send('log test');
});

// 意図的にエラーを起こすルート
app.get("/error", (req, res) => {
  throw new Error("システムログの出力テスト Errorです");
});

// systemロガー
const systemLogger = require("./lib/log/systemLogger");
// ロガーをExpressに実装
app.use(systemLogger());

//===
const logger = require("./lib/log/logger").application;
logger.addContext("key", "test");
logger.error("アプリケーションログの出力テスト Errorです");
//===

app.listen(3000);

サーバーを起動すると、test.logという名前のファイルが作成されるはずです。

$ node server.js
# <APP ROOT>/log/application/test.log
[2019-05-06T07:50:08.500] [Level { level: 40000, levelStr: 'ERROR', colour: 'red' }] application - アプリケーションログの出力テスト Errorです

この実装で、アプリケーションログは出力できますが、アプリケーションログだけlogger.addContext()logger.error()の2行を書くのはダサいので修正します。

以下のような指定で出力できるようにしたい。

logger.error("test", "1行で出力できるようにしたい");

ルートロガーを修正します。

アプリケーションロガーを拡張して、アプリケーションID(key)と出力内容(message)を取得して、ログを出力できるようにします。

// <APP ROOT>/lib/log/logger.js
const log4js = require("log4js");
// log4jsの中からlevelを設定しているファイルを指定
// https://github.com/log4js-node/log4js-node/blob/master/lib/levels.js を参照
const levels = require("log4js/lib/levels").levels;
const config = require("../../config/log4js-config.js");
log4js.configure(config);

// それぞれのログ種別ごとに作成
const console = log4js.getLogger();
const system = log4js.getLogger("system");

// アプリケーションロガー拡張
const ApplicationLogger = function () {
  this.logger = log4js.getLogger("application");
};
const proto = ApplicationLogger.prototype;
for (let level of levels) {
  // log4jsのソースコード見ると、大文字になっているので小文字にします。
  level = level.toString().toLowerCase();
  proto[level] = (function (level) {
    return function (key, message) {
      const logger = this.logger;
      logger.addContext("key", key); // logger.Context("key", "test") で実装していたところをこちらで任意の値が設定できるようにする
      logger[level](message);
    };
  })(level);
}

// 新たにロガーを生成
const application = new ApplicationLogger();

// ログ種別のエクスポート
module.exports = {
  console,
  system,
  application,
};

Expressアプリケーションを修正します。

// <APP ROOT>/server.js
const logger = require("./lib/log/logger").application; // ADD
const express = require("express");
const app = express();

app.get('/', (req, res) => {
  logger.debug('デバッグログが出力されます');
  res.send('log test');
});

// 意図的にエラーを起こすルート
app.get("/error", (req, res) => {
  throw new Error("システムログの出力テスト Errorです");
});

// systemロガー
const systemLogger = require("./lib/log/systemLogger");
// ロガーをExpressに実装
app.use(systemLogger());

//===
logger.error("test", "1行で出力できるようにしたい"); // 今度はkeyとメッセージのみで出力できる
logger.error("app1","こちらは別のログファイルで出力される");  // ついでなのでこちらの確認もします
//===

app.listen(3000);

以下のログが出力されるはずです。

# <APP ROOT>/log/application/test.log
[2019-05-06T08:14:33.930] [Level { level: 40000, levelStr: 'ERROR', colour: 'red' }] application - 1行で出力できるようにしたい

# <APP ROOT>/log/application/app1.log
[2019-05-06T08:18:45.057] [Level { level: 40000, levelStr: 'ERROR', colour: 'red' }] application - こちらは別のログファイルで出力される

以上でアプリケーションログの実装は完了です。

アクセスログの実装

アクセスログの要件は以下の通りです。

・INFOログを出力。
・ファイルは日付でローテーションする。
・/log/access/access.log に出力する。
・世代管理は5ファイル(5日分)までとする。

Date Rolling File Appenderを使う

アクセスログの実装には、Date Rolling File Appenderを使用します。
Date Rolling File Appenderで指定できる設定は以下の通りです。

設定 詳細
type string 設定できる値は、dateFile のみ。
filename string 出力ファイルのパスを設定。
pattern string ログローテーションする際にサフィックス。(yyyy-MM-dd 等)
daysTokeep number ログファイル名のサフィックスを設定。(拡張子のこと)
layout object ログ出力のレイアウトを指定。(日付のフォーマット等の設定)
compress boolean ログファイルの保持数が上限に達した場合に古いログを削除するかの設定(trueに設定すると削除)
keepFileExt boolean patternで指定したサフィックスを.logの後につけるか前につけるかを指定。

Expressでアクセスログを出力するために

公式ドキュメントを見ると、log4js.connectLogger(logger,options)で指定できるようです。
loggerにはログ出力に使用するロガーを指定、optionsにはログレベルやログフォーマットを指定します。

optionsは指定できるものが多いので、まとめました。

①levelオプション

詳細
ログレベルを指定する。
"auto"または任意の値が設定できそうです。
"auto"を指定した場合、
・3xx→WARNログ
・4xx、5xx→ERRORログ
・その他→INFOログ
として出力されます。

②formatオプション

設定値 詳細
:date[フォーマット] 現在日時(サーバーサイド)、format指定にはelf/iso/webのいずれかを指定。
:http-version リクエストHTTPのバージョンを表示。
:method リクエストメソッド(POST、GET)を表示。
:referrer リクエストのリファラを表示。
:remote-addr リクエストのリモートアドレスを表示。
:remote-user Basic認証を使用していた場合、リクエストユーザー名を表示。
:req[取得したいリクエストヘッダー] 指定したリクエストヘッダーを表示。
:res[取得したいレスポンスヘッダー] 指定したレスポンスヘッダーを表示。
:response-time[桁数指定] レスポンス時間を指定した桁数で表示。
:status レスポンスステータスを表示。
:url リクエストURLを表示。
:user-agent リクエストのUser-Agentを表示。

実装

ログ設定モジュールにアクセスログ用の設定を追加します。

// <APP ROOT>/config/log4js-config.js
const path = require('path');
// ログ出力先は、サーバー内の絶対パスを動的に取得して出力先を設定したい
const APP_ROOT = path.join(__dirname, '../');

// ログ出力設定
module.exports = {
  appenders: {
    consoleLog: {
      type: 'console',
    },
    systemLog: {
      type: 'file',
      filename: path.join(APP_ROOT, './log/system/system.log'),
      maxLogSize: 5000000,
      backups: 5,
      compress: true,
    },
    applicationLog: {
      type: "multiFile",
      base: path.join(APP_ROOT, "./log/application/"),
      property: "key",
      extension: ".log",
      maxLogSize: 5000000,
      backups: 5,
      compress: true,
    },
    // ADD
    accessLog: {
      type: "dateFile",
      filename: path.join(APP_ROOT, "./log/access/access.log"),
      pattern: "yyyy-MM-dd", // 日毎にファイル分割
      daysToKeep: 5, // 5日分の世代管理設定
      compress: true,
      keepFileExt: true,
    }
  },
  categories: {
    default: {
      appenders: ["consoleLog"],
      level: "ALL"
    },
    system: {
      appenders: ["systemLog"],
      level: "ERROR"
    },
    application: {
      appenders: ["applicationLog"],
      level: "ERROR"
    },
    // ADD
    access: {
      appenders: ["accessLog"],
      level: "INFO"
    }
  },
};

ルートロガーモジュールに追加します。

// <APP ROOT>/lib/log/logger.js
const log4js = require("log4js");
const levels = require("log4js/lib/levels").levels;
const config = require("../../config/log4js-config.js");
log4js.configure(config);

// それぞれのログ種別ごとに作成
const console = log4js.getLogger();
const system = log4js.getLogger("system");
const access = log4js.getLogger("access");  // ADD

const ApplicationLogger = function () {
  this.logger = log4js.getLogger("application");
};
const proto = ApplicationLogger.prototype;
for (let level of levels) {
  level = level.toString().toLowerCase();
  proto[level] = (function (level) {
    return function (key, message) {
      const logger = this.logger;
      logger.addContext("key", key);
      logger[level](message);
    };
  })(level);
}

const application = new ApplicationLogger();

// ログ種別のエクスポート
module.exports = {
  console,
  system,
  application,
  access,      // ADD
};

ログ出力モジュールは、ルートロガーモジュールとは別に作成します。

// <APP ROOT>/lib/logger/accessLogger.js
const log4js = require("log4js");
const logger = require("./logger").access;

module.exports = (options) => {
  options = options || {};  // オプションを指定する場合はそちらを使う
  options.level = options.level || "auto";  // ない場合、autoを設定
  return log4js.connectLogger(logger, options);  // ログ設定 Expressのアクセスログと結びつける
};

Expressアプリに実装します。

loggerの設定で、levelにautoを設定したので、ステータス毎にどのように出力されるかも確認してみます。

// <APP ROOT>/server.js
const accessLogger = require("./lib/log/accessLogger");  // ADD
const logger = require("./lib/log/logger").application;
const systemLogger = require("./lib/log/systemLogger");
const express = require("express");
const app = express();
app.use(systemLogger());
// ADD
app.use(accessLogger());

app.get('/', (req, res) => {
  logger.debug('デバッグログが出力されます');
  res.send('log test');
});

// ADD
app.get("/access1", (req, res) => {
  res.status(200).send("access test 200");
});
// ADD
app.get("/access2", (req, res) => {
  res.status(304).send("access test 304");
});
// ADD
app.get("/access3", (req, res) => {
  res.status(404).send("access test 404");
});
// ADD
app.get("/access4", (req, res) => {
  res.status(500).send("access test 500");
});

app.get("/error", (req, res) => {
  throw new Error("システムログの出力テスト Errorです");
});

app.listen(3000);

curlでaccess1~4にアクセスすると、以下のようにステータス毎にログレベルの表示が変化します。

# <APP ROOT>/log/access/access.log
[2019-05-06T09:39:28.034] [Level { level: 20000, levelStr: 'INFO', colour: 'green' }] access - ::1 - - "GET /access1 HTTP/1.1" 200 15 "" "curl/7.54.0"
[2019-05-06T09:40:06.079] [Level { level: 30000, levelStr: 'WARN', colour: 'yellow' }] access - ::1 - - "GET /access2 HTTP/1.1" 304 - "" "curl/7.54.0"
[2019-05-06T09:40:10.745] [Level { level: 40000, levelStr: 'ERROR', colour: 'red' }] access - ::1 - - "GET /access3 HTTP/1.1" 404 15 "" "curl/7.54.0"
[2019-05-06T09:40:16.519] [Level { level: 40000, levelStr: 'ERROR', colour: 'red' }] access - ::1 - - "GET /access4 HTTP/1.1" 500 15 "" "curl/7.54.0"

以上でアクセスログの実装は完了です。

今回作成したデモアプリはこちら に格納しました。

参考資料

紹介しきれていない設定がまだあるので、こちらを参考にすると細かい設定ができると思います。

log4jsの公式マニュアル
GitHub上にあるdocs

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

今更ながらSlackBotを作ってみた

今回は、SlackBotを作ってみます。
いまさらながらな感はありますが、Slackアプリをスマホに常時入れているし、LINEと違って、いつでもメッセージを送信できるからです。(LINEの場合好きな時に送るには有料あるいはTrial版である必要があったります)

今回はまずはエコーバックするところまでです。

(補足)
ただし、1点だけ、残念なことがありまして、AWS Lambdaでも同じように動くようにしたかったのですが、現時点では不十分です。SlackからのWebhook呼び出しに対する応答時間の制限が短いためです。うまくやればできるのかもしれませんが、まだAWSの勉強不足のため、また今度にしたいと思います。
ですので、とりあえず、ローカルに立ち上げるSwagger環境を前提とします。

Swagger環境は、以下の投稿を参考にしてください。
 SwaggerでLambdaのデバッグ環境を作る(1)

以下、参考情報です。

slack api
 https://api.slack.com/

Node Slack SDK
 https://github.com/slackapi/node-slack-sdk

以下、投稿の続編です。
 今更ながらSlackBotを作ってみた:Slach Commands
 今更ながらSlackBotを作ってみた:Interactive Components
 今更ながらSlackBotを作ってみた:ダイアログ
 今更ながらSlackBotを作ってみた:Incoming Webhooks

ワークスペースを作成する

まずは、まだワークスペースを作成していない場合は作成しておきます。

slack
 https://slack.com/intl/ja-jp/

image.png

右上のワークスペースボタンから、「ワークスペースを作成する」を選択

image.png

ご自身のメールアドレスを入力して、Next→ を押下

image.png

そうすると、メールアドレスに、3+3の確認コードの数字が送られてきますので、入力します。

image.png

たとえば、TestTream とでもしておきます。

image.png

TestProjectとでもしておきます。

image.png

あとでも招待できるので、skip for now します。

image.png

できあがりました。
「新規登録する」ボタンを押下します。

image.png

表示させたい自分の名前と、次にログインするときのパスワードを入力します。

image.png

SlackのURLは、自動的に割り振られていますが、以降使うので、(他人と重ならなければ)変えることができます。

image.png

招待は後回しに、「完了」ボタンを押下で完了です。

Botを作成する

さっそくBotを作成していきます。
まずは、slack apiを開きます。
 https://api.slack.com/

image.png

次にページの右上にある「Your Apps」をクリックします。

image.png

そして、「Create New App」ボタンを押下します。
AppNameには適当に「TestBot」とでもしておきます。
Development Slack Workspaceには、先ほど作成したワークスペースを選択します。

image.png

作成が完了し、以下のページが表示されます。Setting-Basic Informationです。
以降このページを中心に設定を進めます。

image.png

作ったAppをBotにするため、「Bots」を選択します。

image.png

「Add a Bot User」ボタンを押下します。

image.png

DisplayNameに「TestBot」、Default usernameには「testbot」とでもしておきます。
Always Show My Bots as Onlineは、常時サーバを立ち上げるので、Onにしておきましょう。
最後に「Add Bot User」ボタンを押下します。

今のままだと何もできないので、メッセージ送信できるようにします。

Setting-Basic Information に戻ります。
Add features and functionality を選択します。
「Permmissioins」を選択します。

image.png

ScopesのSelect Permission Scopesのところで、以下を選択します。
「Send message as user」

最後に、「Save Changes」ボタンを押下します。
同じページの最初に戻って、「Install App to Workspace」を押下します。

image.png

そうすると、許可を確認する画面が表示されます。「許可する」を押下します。

image.png

これで、ワークスペース「TestTeam」に「TestBot」が追加されました。

WebAPIの種類

Slackにはメッセージ通信するAPIとしていくつかの種類があります。
以下、私なりの理解です。

  • Incoming Webhook
     いつでも好きな時に、特定チャネルまたは特定ユーザにメッセージを送信します。(受信ではない)

  • Slash Command
     「/」(スラッシュ)に続けて入力するコマンドを作ります。

  • RTM(Real Time Messaging) API
     WebSocketを使って、リアルタイムにイベントを受信したりメッセージを送信します。

  • Event API
     メッセージ投稿やメンバー追加などのイベント発生時に通知を受け取り、メッセージで応答します。

  • Interactive Components
     上記のメッセージ送受信に加えて、ボタンや選択肢などのリッチなUIを提供します。(ダイアログもこれに該当?)

今回、次回以降の記事で、ひとつひとつ説明していきます。(RTM APIを除く)

エコーバックするBotにする

まずは、一番単純なエコーバックするBotを作成しましょう。
先に、エコーバックするためのサーバを立ち上げます。

その前に、メモっておく必要がある文字列が2つあります。
まずは、Settings-Basic Informationの、App Credentialsにある「Verification Token」です。

image.png

次に、Features - OAuth&Permissions にある「Bot User OAuth Access Token」です。

image.png

それでは、サーバの立ち上げを始めます。

以下のnpmモジュールを使います。
・@slack/web-api
・node-fetch
・dotenv

以下の2つのエンドポイントを作成します。
この2つの違いは、入出力パラメータのフォーマットの違いです。メッセージを送受信する方法はいくつかあるのですが、それぞれによって微妙にフォーマットが違うのです。

swagger.yaml
  /slack-testbot:
    post:
      x-swagger-router-controller: routing
      operationId: slack-testbot
      produces:
        - text/plain
      parameters:
        - in: body
          name: body
          schema:
            type: object
      responses:
        200:
          description: Success
          schema:
            type: string

  /slack-testbot-cmd:
    post:
      x-swagger-router-controller: routing
      operationId: slack-testbot-cmd
      consumes:
        - application/x-www-form-urlencoded
      produces:
        - text/plain
      parameters:
        - in: body
          name: body
          schema:
            type: object
      responses:
        200:
          description: Success
          schema:
            type: string
・・・

エンドポイントから実装へのルーティングです。

controllers\routing.js
・・・
/* 関数を以下に追加する */
const func_table = {
//  "test-func" : require('./test_func').handler,
  "slack-testbot" : require('./slack_testbot').handler,
  "slack-testbot-cmd" : require('./slack_testbot').handler,
};
・・・

エンドポイントに対応する実装です。

controllers\slack_testbot\index.js
'use strict';

const SLACK_VERIFICATION_TOKEN = process.env.SLACK_VERIFICATION_TOKEN;
const SLACK_ACCESS_TOKEN = process.env.SLACK_ACCESS_TOKEN;

const HELPER_BASE = process.env.HELPER_BASE || '../../helpers/';
const SlackUtils = require(HELPER_BASE + 'slack-utils');
const app = new SlackUtils(SLACK_VERIFICATION_TOKEN, SLACK_ACCESS_TOKEN);

exports.handler = app.lambda();

Slackボットのためのユーティリティです。

controllers\helpers\slack-utils.js
'use strict';

const fetch = require('node-fetch');
const { WebClient } = require('@slack/web-api');

class SlackUtils{
    constructor(verification_token, access_token){
        this.web = new WebClient(access_token);
        this.verification_token = verification_token;
        this.map = new Map();
    }

    async initialize(){
        if( this.my_bot_id )
            return Promise.resolve();

        return this.web.auth.test()
        .then(result =>{
            this.my_user_id = result.user_id;
            console.log('userid=' + this.my_user_id);
            return this.web.users.info({user: result.user_id})
        })
        .then(result =>{
            this.my_app_id = result.user.profile.api_app_id;
            console.log('appid=' + this.my_app_id);
            return this.web.bots.info({bot: result.user.profile.bot_id})
        })
        .then(result =>{
            this.my_bot_id = result.bot.id;
            console.log('botid=' + this.my_bot_id);
        })
        .catch(error =>{
            console.log(error);
        });
    }

    postMessage(message){
        return this.web.chat.postMessage(message);
    }

    dialogOpen(options){
        return this.web.dialog.open(options);
    }

    incomingMessage(webhook_url, body){
        return this.responseMessage(webhook_url, body);        
    }

    responseMessage(response_url, body){
        return fetch(response_url, {
            method : 'POST',
            body : JSON.stringify(body),
            headers: { "Content-Type" : "application/json; charset=utf-8" } 
        })
        .then((response) => {
            if( response.status != 200 )
                throw 'status is not 200';
            return response.text();
        });
    }

    ackResponse(){
        var response = {
            statusCode: 200,
            headers: { "Content-Type": "text/plain" },
            body: ""
        };
        return response;
    }

    message(handler){
        this.map.set('message', handler);
    }

    action(handler){
        this.map.set('action', handler);
    }

    response(handler){
        this.map.set('response', handler);
    }

    command(handler){
        this.map.set('command', handler);
    }

    submission(handler){
        this.map.set('submission', handler);
    }

    cancellation(handler){
        this.map.set('cancellation', handler);
    }

    lambda(){
        return async (event, context, callback) => {
            await this.initialize();

            var body = JSON.parse(event.body);
            if( body.payload )
                body = JSON.parse(body.payload);

            if( body.token != this.verification_token )
                return;

            if( body.type == 'url_verification' ){
                var response = {
                    statusCode: 200,
                    headers: { "Content-Type": "text/plain" },
                    body: body.challenge
                };
                return response;                
            }

            console.log('body.user_id', body.user_id);
            if( body.event )
                console.log('body.event.user', body.event.user);
            if( body.user )
                console.log('body.user.id', body.user.id);
            console.log(JSON.stringify(body));

            if( body.user_id == this.my_user_id || (body.event && body.event.user == this.my_user_id ) )
                return this.ackResponse();

            var type = 'message';
            if( body.event && body.event.message )
                type = 'response';
            if( body.command )
                type = 'command';
            if( body.type == 'block_actions' )
                type = "action";
            if( body.type == 'dialog_submission')
                type = "submission";
            if( body.type == 'dialog_cancellation')
                type = "cancellation";

            var handler = this.map.get(type);
            if( handler )
                handler(body, this.web);
            else
                console.log(type + ' is not defined.');

            callback(null, this.ackResponse());
        }
    }
};

module.exports = SlackUtils;

環境変数に、各人のVerification TokenとBot User OAuth Access Tokenを設定します。

SLACK_VERIFICATION_TOKEN="【Verification Token】"
SLACK_ACCESS_TOKEN="【Bot User OAuth Access Token】"

さっそく立ち上げておきます。

エコーバックのトリガとしては、Event APIを使っていきます。
以下の手順で有効にします。

・Features-Event Subscriptions を選択し、Enable EventsをOnにします。
・そして、Request URLのところに、先ほど立ち上げたサーバのエンドポイントをフルパスで指定します。/slack-testbotの方です。

image.png

無事に、「Verified」となりましたでしょうか?slack-utils.jsがVerifyに必要な処理をしています。

次に、Subscribe to Bot Events に、「message.im」を選択し、最後に右下の「Save Changes」を押下します。
これにより、メッセージを受信したときに、Eventが発火されるようになります。
そこら辺のハンドリングは、slack-utils.jsがうまくやってくれるようにしておきました。

発火されたEventは、app.message()に指定したコールバックを呼び出します。
したがって、このコールバック関数内に、処理したい内容(今回はエコーバック)を記述します。
index.jsを少し追記します。

controllers\slack_testbot\index.js
'use strict';

const SLACK_VERIFICATION_TOKEN = process.env.SLACK_VERIFICATION_TOKEN;
const SLACK_ACCESS_TOKEN = process.env.SLACK_ACCESS_TOKEN;

const HELPER_BASE = process.env.HELPER_BASE || '../../helpers/';
const SlackUtils = require(HELPER_BASE + 'slack-utils');
const app = new SlackUtils(SLACK_VERIFICATION_TOKEN, SLACK_ACCESS_TOKEN);

// 追記ここから
app.message(async (body, web) =>{
    if(body.event.text)
        app.postMessage({ channel: body.event.channel, text: body.event.text + " です。", as_user: true });
});
// 追記ここまで

exports.handler = app.lambda();

ちょっと補足します。

受信しているメッセージは以下を参考にしてください。

slack api:Event API:Receiving Events
 https://api.slack.com/events-api#receiving_events
slack api:message.im
 https://api.slack.com/events/message.im

返信している内容は以下を参考にしてください。

slack api:Reference: Message payloads
 https://api.slack.com/reference/messaging/payload

動作確認

スマホで動作確認してみましょう。
以降は、AndroidのSlackでの画面です。

左上隅をタッチします。

image.png

ワークスペースを追加 を選択します

image.png

ワークスペース「TestTeam」を作った人と同じであれば、そのワークスペースが表れていると思います。
サインインを押下します。

image.png

さっきのパスワードを入力します。

image.png

ログインできました。
左上隅をタッチし、TestBotを選択します。

image.png

image.png

TestBotが開きましたので、「あかさたな」と投稿してみましょう。

image.png

「あかさたな です」と返ってくれば成功です。

終わりに

今回はここまでとします。
次回から、ボタンや選択肢を表示してインタラクションを高めたり、ダイアログボックスを表示したり、スラッシュコマンドを追加したりしてみます。

SlackのAPIは多数あり、わかりにくいです。
ですので、もしかしたら使い方が間違っているかもしれませんので、ご指摘いただけると助かります。

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む