- 投稿日:2019-05-06T23:39:20+09:00
「Hexo 内部探訪」のご紹介
静的サイト・ジェネレータのひとつである、Hexo。
最近、ブログを書くために使い始めました。使うというだけでなく、プラグイン機構を活かし、開発もはじめました。
hexo-tag-google-photos-album 公開しました
まだまだ荒削りな部分が残りますが、さしあたり使う分には困らない程度まではできました。今後、プラグインの開発を進めるにあたり、方法や内部構造などを調べています。
それで、成果(?)をアウトプットしていこうと考え、まとめています。
また、それを知っていただいたり、フィードバックなどを得られたり、ということを期待して、Qiitaにも書いてみようかと思いました。中身は私のブログに、少しずつ記事をあげてますので、ご興味あればご覧ください。(宣伝乙)
- カテゴリー hexo
- Hexo 内部探訪 (1) はじめに
- Hexo 内部探訪 (2) 準備 npm linkメモ
- Hexo 内部探訪 (3) コンテキストとしての"hexo"変数
- Hexo 内部探訪 (4) プラグインのロード
- Hexo 内部探訪 (5) ライフサイクル
- Hexo 内部探訪 (6) DevToolsを使ったデバッグ
今後も連載継続予定。
- 投稿日:2019-05-06T23:02:35+09:00
テンプレート作成
初めに
前回は生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; }完成図
完成図です。顔はマスキングしてますのであしからず
総括
って感じで一応見せる用のサイトは作成できました。
シンプルすぎますがw
一応次はドメイン取得して外部公開方法予定です。
- 投稿日:2019-05-06T21:14:50+09:00
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.confupstream 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 nginxport 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/
- 投稿日:2019-05-06T16:47:14+09:00
ETHのtestnet faucetを作る with firebase cloud functions
はじめに
FirebaseのCloud Functionsを使ってNode.js(TypeScript)のETH testnet用トークン配布システムを作ります
事前準備
まず、何らかの方法でETHのアドレスと秘密鍵を用意してください
あらかじめtestnet(ここではrinkeby)のETHを入れておきましょうセットアップ
firebaseプロジェクトをこちらから作ってください
firebaseのCLI入れてない方は入れましょう
npm install -g firebase-toolsmkdir eth-faucet cd eth-faucetfirebase login firebase initその後「Select a default Firebase project for this directory:」と言われるので先ほど作ったFirebaseのプロジェクトを選択
※反映されてない場合は「don't setup a default project」を選択し、あとで
firebase use --add [project-id]
とすればOKTypeScriptを選択しTSLintも導入してもらいましょう
最後にFirebase Consoleでプロジェクトの課金設定をBlazeにします
理由は無料版だとfirebase関連以外の外部パッケージが使えないからです
作っていく
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.tsimport * 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でみるときちんと送金はされています
- 投稿日:2019-05-06T16:25:14+09:00
今更ながらSlackBotを作ってみた:Incoming Webhooks
SlackBotを作ってきましたが、これがいよいよ最後です。
今更ながらSlackBotを作ってみた
今更ながらSlackBotを作ってみた:Slach Commands
今更ながらSlackBotを作ってみた:Interactive Components
今更ながらSlackBotを作ってみた:ダイアログ今回はIncomming Webhooksです。
いつでも好きな時に、特定のチャネルや特定のユーザにメッセージを送信します。以下が参考になります。
Incoming Webhooks
https://api.slack.com/incoming-webhooksIncoming Webhooksを有効にする
いつもの、Setting-Basic Informationを開きます。
Incoming Webhooksを選択します。
Activate Incoming WebhooksをOnにします。
次に、下の方にある「Add New Webhhooks to Workspace」リンクをクリックします。
投稿先として、メッセージを送信したいチャネルまたはユーザ名を選択し、許可するボタンを押下します。
例えば、最初に作ったチャネル#testprojectを選択しました。そうすると、Webhook URLのテーブルに先ほどのチャネルとURLが追加されているのがわかります。Webhook URLは後で使うので覚えておきます。
サーバ側の実装
Incoming Webhookによるメッセージ送信するきっかけとして、前回の投稿で作ったアンケートへの回答があった時としたいと思います。
app.submissionは変更、それ以外は追加です。
controllers\slack_testbot\index.jsconst 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アプリで動作確認しました。
こんな感じで入力して、送信すると、
レスポンスメッセージを受信しました。
ここまでは以前と同じですが、チャネル#testprojectにもメッセージが届いています。
以上
- 投稿日:2019-05-06T14:51:25+09:00
今更ながらSlackBotを作ってみた:ダイアログ
前回からの続きで、SlackBotをカスタマイズしていきます。
今更ながらSlackBotを作ってみた
今更ながらSlackBotを作ってみた:Slach Commands
今更ながらSlackBotを作ってみた:Interactive Components今回はダイアログです。
何かの申請書の入力だったり、アンケート回答がわかりやすいかもしれません。詳細は以下を参照してください。
slack api: Interacting with users through dialogs
https://api.slack.com/dialogsPermission設定
前回までを進めていただいている場合は、特に追加の権限付与は不要です。
サーバ側の実装
の前に、ダイアログを表示するきっかけとして、SlashCommandsを設定します。
(ダイアログ表示の際には、trigger_idが必要です。SlashCommandsからのメッセージにはそれが含まれています)Features-Slash Commandsを開きます。
Create New Commandボタンを押下して、作成します。
サーバ側の実装です。
app.commandは変更、それ以外は追加です。controllers\slack_testbot\index.jsapp.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を入力すると、
ダイアログが表示されました!
こんな感じで入力して、右上の送信するを押下すると、
app.submissionが呼ばれ、レスポンスメッセージが返されました。
サーバ側では、app.submissionのコールバック関数に、以下のデータが返ってきています。
"submission": { "name": "ハンバーグ", "review": "very_good", "others": null },ちなみに、送信せずに、戻る をすると、以下のようにapp.cancelleationが呼ばれ、レスポンスメッセージが返ってきます。
以上
- 投稿日:2019-05-06T14:05:11+09:00
今更ながらSlackBotを作ってみた:Interactive Components
前回、前々回に引き続き、SlackBotをカスタマイズしていきます。
今更ながらSlackBotを作ってみた
今更ながらSlackBotを作ってみた:Slach Commands今回は、Interactive Componentsです。
絵文字や文字装飾でもある程度表現できますが、やはりボタンやセレクトや画像を使った方がより直感的にやりとりできます。前々回の環境をそのまま使います。まだの方はぜひご参照ください。
(参考情報)
Making messages interactive
https://api.slack.com/interactive-messagesInteractive Componentsを有効化する
いつものslack apiのSettings-Basic Informationから行います。
Interactive Componentsを選択し、InteractivityをOnにします。
入力欄「Request URL」に立ち上げたサーバのURLをフルパスで入力します。「/slack-testbot-cmd」の方です。最後にSave Changesボタンを押下します。
これで、表現力豊かなUIからの通知を受けることができました。
UIを作成する
UIを配置していきたいのですが、便利なツールがあります。
GUIで配置結果を見ながら作れるので便利です。(少々使いにくいですが)Block Kit Builder
https://api.slack.com/tools/block-kit-builder以下のようなUIを作成しました。
[ { "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」というコマンドを作ってみましょう
それではさっそく、サーバ側の実装です。
app.commandは変更で、それ以外は追加です。controllers\slack_testbot\index.jsapp.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コマンドを入力すると、
UIが表示されました。
解像度が小さいスマホで表示させたので、解像度が大きい場合は見え方が違うと思います。選択肢を選択すると
という感じに、レスポンスメッセージが返ってきました。
補足
UIの各部品が選択されるごとに、メッセージが通知されました。
次回は、ダイアログを紹介します。都度通知ではなくすべての入力完了時に通知されるようになります。以上
- 投稿日:2019-05-06T13:57:32+09:00
ETHのアカウントを作る
概要
Ethreumのアカウントをweb3.jsを使って作成します
作り方
mkdir create-eth-account cd create-eth-account touch create.js次にweb3をインストールします
npm i --save web3あとはcreate.jsファイルを以下のように編集します
create.jsconst 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-06T12:26:43+09:00
今更ながらSlackBotを作ってみた:Slach Commands
前回に引き続き、SlackBotをカスタマイズしていきます。
今更ながらSlackBotを作ってみた今回は、スラッシュコマンドです。
Slackには、/で始まるコマンドがたくさん用意されていますよね。
そこに、独自のコマンドを付け加えることができ、それをSlackBotが処理できるようになります。前回の環境をそのまま使います。まだの方はぜひご参照ください。
SlashCommandsを作成する
Setting-Basic Information のページを表示し、Slash Commandsを選択します。
それでは、「Create New Command」ボタンを押下しましょう。
「hi」という呼びかけに対して、あいさつ文と一緒にエコーバックするコマンドを作ってみましょう。例えば、以下のような感じで作成してみました。
・Command:/hi
・Request URL : 立ち上げたサーバのエンドポイントをフルパスで指定します。/testbot-cmdの方です。
・Short Description:やあ
・Usage Hint:呼びかけに応答します。「Save」ボタンを押下します。
インストール済みのアプリを更新する必要がありますので、click hereのリンクをクリックします。あるいは、Setting-Install Appを選択して、「Reinstall App」ボタンを押下します。すると、以下のような確認ページが表示されるので、「許可する」ボタンを押下します。
これで、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動作確認
こんな感じでできました。
入力中。
応答を受信。
SlackBot用にユーティリティslack-utils.jsを用意しておいたので、わりと簡単に実装できるようになったのではないかと思います。
以上
- 投稿日:2019-05-06T12:26:43+09:00
今更ながらSlackBotを作ってみた:Slash Commands
前回に引き続き、SlackBotをカスタマイズしていきます。
今更ながらSlackBotを作ってみた今回は、スラッシュコマンドです。
Slackには、/で始まるコマンドがたくさん用意されていますよね。
そこに、独自のコマンドを付け加えることができ、それをSlackBotが処理できるようになります。前回の環境をそのまま使います。まだの方はぜひご参照ください。
(参考情報)
slack api:Slash Commands
https://api.slack.com/slash-commandsSlashCommandsを作成する
Setting-Basic Information のページを表示し、Slash Commandsを選択します。
それでは、「Create New Command」ボタンを押下しましょう。
「hi」という呼びかけに対して、あいさつ文と一緒にエコーバックするコマンドを作ってみましょう。例えば、以下のような感じで作成してみました。
・Command:/hi
・Request URL : 立ち上げたサーバのエンドポイントをフルパスで指定します。/testbot-cmdの方です。
・Short Description:やあ
・Usage Hint:呼びかけに応答します。「Save」ボタンを押下します。
インストール済みのアプリを更新する必要がありますので、click hereのリンクをクリックします。あるいは、Setting-Install Appを選択して、「Reinstall App」ボタンを押下します。すると、以下のような確認ページが表示されるので、「許可する」ボタンを押下します。
これで、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動作確認
こんな感じでできました。
入力中。
応答を受信。
SlackBot用にユーティリティslack-utils.jsを用意しておいたので、わりと簡単に実装できるようになったのではないかと思います。
以上
- 投稿日:2019-05-06T11:17:40+09:00
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.0Angularで新規プロジェクトを作成すると脆弱性が指摘される
新規プロジェクト作成
$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
- 投稿日:2019-05-06T10:12:49+09:00
Express v4 と log4js v4 で実践的なロガーを実装
はじめに
node.jsでアプリケーションログを出力する際に、業務でありがちなログのローテーションや、ログレベル毎にファイルを分けるとかの設定をlog4jsで実装してみました。
実装時の環境
以下の環境で実装しました。
・OS:Mac OS X v10.14.4
・Node:v12.1.0
・npm:v6.9.0log4jsで指定できるログの種別
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/errorcurlでアクセスすると、以下のようなログが出力されます。
また、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/errorsystem.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"以上でアクセスログの実装は完了です。
今回作成したデモアプリはこちら に格納しました。
参考資料
紹介しきれていない設定がまだあるので、こちらを参考にすると細かい設定ができると思います。
- 投稿日:2019-05-06T02:53:45+09:00
今更ながら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/右上のワークスペースボタンから、「ワークスペースを作成する」を選択
ご自身のメールアドレスを入力して、Next→ を押下
そうすると、メールアドレスに、3+3の確認コードの数字が送られてきますので、入力します。
たとえば、TestTream とでもしておきます。
TestProjectとでもしておきます。
あとでも招待できるので、skip for now します。
できあがりました。
「新規登録する」ボタンを押下します。表示させたい自分の名前と、次にログインするときのパスワードを入力します。
SlackのURLは、自動的に割り振られていますが、以降使うので、(他人と重ならなければ)変えることができます。
招待は後回しに、「完了」ボタンを押下で完了です。
Botを作成する
さっそくBotを作成していきます。
まずは、slack apiを開きます。
https://api.slack.com/次にページの右上にある「Your Apps」をクリックします。
そして、「Create New App」ボタンを押下します。
AppNameには適当に「TestBot」とでもしておきます。
Development Slack Workspaceには、先ほど作成したワークスペースを選択します。作成が完了し、以下のページが表示されます。Setting-Basic Informationです。
以降このページを中心に設定を進めます。作ったAppをBotにするため、「Bots」を選択します。
「Add a Bot User」ボタンを押下します。
DisplayNameに「TestBot」、Default usernameには「testbot」とでもしておきます。
Always Show My Bots as Onlineは、常時サーバを立ち上げるので、Onにしておきましょう。
最後に「Add Bot User」ボタンを押下します。今のままだと何もできないので、メッセージ送信できるようにします。
Setting-Basic Information に戻ります。
Add features and functionality を選択します。
「Permmissioins」を選択します。ScopesのSelect Permission Scopesのところで、以下を選択します。
「Send message as user」最後に、「Save Changes」ボタンを押下します。
同じページの最初に戻って、「Install App to Workspace」を押下します。そうすると、許可を確認する画面が表示されます。「許可する」を押下します。
これで、ワークスペース「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」です。次に、Features - OAuth&Permissions にある「Bot User OAuth Access Token」です。
それでは、サーバの立ち上げを始めます。
以下の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の方です。無事に、「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での画面です。左上隅をタッチします。
ワークスペースを追加 を選択します
ワークスペース「TestTeam」を作った人と同じであれば、そのワークスペースが表れていると思います。
サインインを押下します。さっきのパスワードを入力します。
ログインできました。
左上隅をタッチし、TestBotを選択します。TestBotが開きましたので、「あかさたな」と投稿してみましょう。
「あかさたな です」と返ってくれば成功です。
終わりに
今回はここまでとします。
次回から、ボタンや選択肢を表示してインタラクションを高めたり、ダイアログボックスを表示したり、スラッシュコマンドを追加したりしてみます。SlackのAPIは多数あり、わかりにくいです。
ですので、もしかしたら使い方が間違っているかもしれませんので、ご指摘いただけると助かります。以上