- 投稿日:2021-01-29T22:06:52+09:00
PuppeteerでローカルのHTMLファイルをData URI Schemeとして読み込む
Puppeteerの
page.goto()
はhttpプロトコルやfileプロトコルなどの他、Data URI scheme文字列も引数にできる。下記のようなHTMLファイルを用意しておき
test.html<html> <head> <meta charset="utf-8"/> </head> <body> Data URI schemeを表示できます </body> </html>同じディレクトリのJavaScriptからこのように読み込むと、
import fs from 'fs/promises'; import puppeteer from 'puppeteer'; const html = "./test.html" const buffer = await fs.readFile(html) const browser = await puppeteer.launch({ headless: false, }); const page = (await browser.pages())[0]; await page.goto(`data:text/html;base64,${buffer.toString("base64")}`); // ... browser.close();Chromiumで開いてくれる。
実運用ではまず使わないけど、ちょっとした確認をするときに便利だったりする。なお、日本語が含まれていると、高確率で文字化けするので、charset指定をしておくのが無難。1
ファイルをそのままChromiumで開いたら文字化けしなくても、なぜかData URI schemeだと化ける。なぜだ? ↩
- 投稿日:2021-01-29T17:21:47+09:00
Node-REDでSlackのSocket Modeを使ってみた
SlackのSocket Modeを使ってNode-RED上でSlackのイベントを取得するノードを実装したので、その紹介記事になります。
想定読者
- Slackの投稿内容などを活用したアプリケーション(Botや他のツールとの連携など)の開発を考えている方。
- Node-REDの動かし方やNode-RED上での実装方法(ノードの配置やワイヤの結線など)をある程度知っている方。
- Node-REDの知識がない方は以下をご参考にどうぞ。
- https://nodered.jp/docs/getting-started/
- https://nodered.jp/docs/tutorials/
- https://qiita.com/egplnt/items/222a3ae289472bb759b1
- 少し古い記事なのでUIなど一部最新のものと異なっている場合があります。
やったこと
SlackでSocket ModeがGAされたので、公式のSDKを使ってNode-REDでSocket Modeを使ったEvents API用のノードを実装&公開しました。
- GitHub:https://github.com/tkscode/node-red-contrib-slack-socket
- npm:https://www.npmjs.com/package/node-red-contrib-slack-socket
- Node-RED flows:https://flows.nodered.org/node/node-red-contrib-slack-socket
開発環境
- Ubuntu 18.04
- 実際はWindows10上のWSL2で動かしています。
- Node.js v12.20
- Docker v20.10
- WSL2 + Docker Desktop for Windowsで動かしています。
- Node-REDを動かすのに利用しています。
- docker-compose v1.27
使い方
Slackアプリケーションの作成
- 公式ドキュメントの"Intro to Socket Mode"を参考にして、Slack用アプリケーションの作成とapp-level tokenの生成、ワークスペースへのアプリケーションのインストールを行います。
- アプリ管理画面の
OAuth & Permissions
->Scopes
->Add an OAuth scope
からイベント取得に必要な権限(スコープ)を設定します。
- Socket Modeを有効にしている場合、デフォルトで
app_mentions:read
(アプリケーション宛てのメンション)が追加されています。- メンションがない通常の投稿を拾いたい場合は
channels:history
(Publicチャンネルの投稿取得)やgroups:history
(Privateチャンネルの投稿取得)を追加します。- アプリ管理画面の
Event Subscriptions
->Subscribe to bot events
->Add Bot User Event
から取得するイベントの種類を選択します。
- メンションを取得するイベントは
app_mention
、Publicチャンネルの投稿イベントはmessage:channels
、Privateチャンネルの投稿イベントはmessage:groups
を選択します。Node-REDの起動とノードの追加
- Node-REDの公式ドキュメントを参考にNode-REDを起動します(今回はDockerで起動しています)。
$ docker volume create nodered-test-vol nodered-test-vol $ docker run -d -p 1880:1880 -v nodered-test-vol:/data -v /etc/localtime:/etc/localtime:ro --name nodered-test nodered/node-red:latest-12-minimal Unable to find image 'nodered/node-red:latest-12-minimal' locally latest-12-minimal: Pulling from nodered/node-red 0a6724ff3fcd: Already exists 5fd2bdfdbf4b: Already exists 80b224d472a8: Already exists e21405c347ae: Already exists b6afffd6ee9d: Pull complete 78320e61ab74: Pull complete ba2a0afb7fb7: Pull complete 09f8757c9445: Pull complete db061e81eb3b: Pull complete 3c59eb56174f: Pull complete a11846f67ffa: Pull complete 0b0bc0c51c26: Pull complete Digest: sha256:4b7d40ab5aa0fe307f4dc7b10c6aa0da47f7001bd41e1ff794dde07bd351534e Status: Downloaded newer image for nodered/node-red:latest-12-minimal d5eb6de063b2c8e1f6523bccf9f91c1c30acb6e7d3054c435c88a0171c9ddb83
ブラウザで http://localhost:1880 にアクセスし、Node-REDの実装画面を開きます。
画面右上のメニュー展開ボタンをクリックし、
パレットの管理
->ノードを追加
で検索バーにnode-red-contrib-slack-socket
を入力し、出てきた項目のノードを追加
ボタンをクリックします。(確認のポップアップが表示されるので追加
をクリックします)
ノードのインストールが完了したら(「ノードをパレットに追加しました」というポップが表示されたら)画面左側のノードが羅列されているエリアの下の
chat
セクションにSlack Listen
があることを確認します。
ノードの設定
画面中央に
Slack Listen
ノードを配置(ドラッグ&ドロップ)し、配置したノードをダブルクリックして設定画面を表示します。
設定画面の
Settings
の行にある編集アイコン(鉛筆アイコン)をクリックしてトークンや取得するイベントの指定画面を表示します以下の通り情報を入力します。
Name
:設定の名前(なんでもOKです)Token
:Slackアプリケーションを作成したときに生成したapp-level token
- OAuthトークンではなく、
xapp-
から始まるトークンになるので注意してください。- なお、デフォルトではトークンを直接入力するようになっていますが、Node-REDのglobalContextや環境変数から指定できるようにもなっています。
- 例えば、環境変数から指定する場合は入力欄左のアイコンをクリックして
$ 環境変数
を選択し、環境変数名(SLACK_APP_TOKEN
など)を指定します。Events
:取得するイベント名
- イベント名はSlackのAPI Event Typesにある
Works with
列にEvent API
と記載があるもの(例:app_mention
やmessage
など)を指定します。- 複数のイベントを指定する場合は
app_mention,message
のようにカンマ区切りで指定します。Slackイベントの取得
Slack Listen
ノードの下にconnected
と表示されることを確認したらSlackからアプリケーション宛てにメンションでメッセージを送ります。
connected
と表示されない場合は、Socket Modeが有効になっているか、トークンが有効なものか(Revokeされていないか、app-level tokenかなど)を確認してください。画面右側にあるデバッグアイコン(虫のアイコン)をクリックし、
Slack Listen
ノードで取得したイベント情報(前項で送られたメッセージの情報)がdebug
ノードから出力されていることを確認します。
Slack Listen
ノードでは取得したイベント情報をそのまま出力しているので、出力内容を事前に知りたい場合はSlackの各Event APIの試用を参照してください。また、取得したイベント情報の加工などを行う場合は適宜
change
ノードやfunction
ノードなどをつないで処理を実装してください。(以下の例ではアプリケーション宛てのメンションからメッセージ部分のみを取得しています)
その他
イベントが取得できない場合は以下を確認してみてください。
Slack Listen
ノードの下にconnected
と表示されているかどうか
connected
以外の場合は設定したトークンが正しいものかどうか確認してください- Slackのアプリ管理画面(
OAuth & Permissions
->Scopes
)にてイベント取得のための権限が十分かどうか- Slackのアプリ管理画面(
Event Subscriptions
->Subscribe to bot events
)にて取得可能なイベントがちゃんと指定されているかどうか参考
- 投稿日:2021-01-29T17:08:04+09:00
【Mac】Nodeインストール手順の備忘録
はじめに
- Mac環境上で、Nodeの開発環境を構築する機会があったので、その備忘録です。
- Nodeの管理ツールとして、nodebrewを使用しました。
Homebrewのインストール
zsh# スクリプト実行 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" # ログ ==> This script will install: /usr/local/bin/brew /usr/local/share/doc/homebrew # ログイン時のPasswordを入力 Password: ==> /usr/bin/sudo /bin/chmod g+rwx /usr/local/bin ︙ Already up-to-date. ==> Installation successful! # インストール確認 brew --version # ログ Homebrew 2.7.7nodebrewのインストール
zsh# インストール実行 brew install nodebrew # Shellの確認 echo $SHELL # → /bin/zsh # .zshrcが無い場合、作成 touch .zshrc # ~/.zshrcにnodebrewのPATHを追記 % echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.zshrc # ファイル読み込み source ~/.zshrc # インストール確認 nodebrew # → nodebrew 1.0.1 # セットアップ実行 nodebrew setupNode.jsのインストール
- インストール可能なバージョンを確認します。
zsh# インストール可能なバージョンを表示 nodebrew ls-remote # → v0.0.1 v0.0.2 ~ # → v15.0.0 v15.1.0 ~ # → io@v1.0.0 io@v2.0.0 ~
- バージョンを指定して、インストールします。
zsh# v12.20.1をインストール nodebrew install v12.20.1
- インストール済みバージョンを確認します。
zshnodebrew list # → v12.20.1
- インストール済みバージョンを有効にします。
zshnodebrew use v12.20.1 node -v # → v12.20.1
- 別バージョンをインストールして、切り替えます。
zsh# v14.15.3をインストール nodebrew install v14.15.3 # インストール済みバージョンを表示 nodebrew list # → v12.20.1 # → v14.15.3 # v14.15.3を有効化 nodebrew use v14.15.3 node -v # → v14.15.3
- 投稿日:2021-01-29T16:47:16+09:00
Puppeteerで使わなれないタブがあるのが気になる
Puppeteerで、よく以下のような書き方を見ます。
const browser = await puppeteer.launch({ headless: false, }); const page = await browser.newPage(); await page.goto("https://www.google.com/");このように書くと、以下のように新しいタブでURLが開かれます。
このとき、利用していない「about:blank」なタブがずっとあるのが気になっていました。(害はないんですが。)
Chromiumって起動時にタブを1個開いているんですよね。Puppeteerのドキュメントを見ると、browser.pages()でページ一覧が取れるので、
const browser = await puppeteer.launch({ headless: false, }); const page = (await browser.pages())[0]; await page.goto("https://www.google.com/");こうすると、もとからあるタブを使ってくれました!
- 投稿日:2021-01-29T11:23:00+09:00
typeORMでセキュアなカラムをselectさせない
typeORMで、パスワード等のカラムを扱う時にうっかりapiのレスポンスに値を含め無い様にする仕組みが用意されています。
何もしない場合
@Entity() export class User { @PrimaryGeneratedColumn() readonly id: number; @Column({ name: 'name', length: 255, }) name: string; @Column('varchar', { name: 'password'}) password: string; }selectすると全カラムが普通に取得されます。うっかりそのままAPIのレスポンスに含めるとまずいです。
除外する
@Entity() export class User { @PrimaryGeneratedColumn() readonly id: number; @Column({ name: 'name', length: 255, }) name: string; @Column('varchar', { name: 'password', select: false}) // ここ password: string; }
{select: false}
というオプションをつければOKです。SQLレベルで除外されます。ちなみに何かしらの理由で敢えて取得したい場合は、明示的に書けばOKです。
const user = await this.userRepository.find({ select: ['password'] });参考
- 投稿日:2021-01-29T09:26:37+09:00
Node.js で Azure BLOB オブジェクト を 30秒間 だけ Read 可能な SAS トークン を ライブラリを使わないで生成する
tl;dr
SAS トークンの構造を学習するために、ライブラリを使わないで、Node.js で生成しました。
( Java 版は こちら )公式ドキュメント : サービス SAS を作成する
コード
BLOB オブジェクト を 30秒 だけ Read 可能なURL
main.jsconst SAS = require("./SAS"); const name = "ストレージアカウント名"; const key = "キー"; const container = "コンテナ名"; const blob = "ブロブ名"; const expiry = 30; // 30 秒間有効 const filename = "ファイル名"; const sas = new SAS(name, key); const url = sas.getUrl(container, blob, expiry, filename); console.log(url);SAS.jsconst crypto = require("crypto"); class SAS { // コンストラクタ // @param name ストレージアカウント名 // @param key ストレージアカウントキー constructor(name, key) { this.account_name = name; this.account_key = key; } // トークン付きURLを生成する // @param container コンテナ名 // @param blob ブロブ名 // @param expiry_second トークン有効期間 // @param filename ファイル名 getUrl(container, blob, expiry_second, filename) { const token = this.getToken(container, blob, expiry_second, filename); const url = `https://${this.account_name}.blob.core.windows.net/${container}/${blob}`; return `${url}?${token}`; } // トークンを生成する // @param container コンテナ名 // @param blob ブロブ名 // @param expiry_second トークン有効期間 // @param filename ファイル名 getToken(container, blob, expiry_second, filename) { // パラメータ const now = new Date(); const start = ""; // this.isoDate(now, start_second); const expiry = this.isoDate(now, expiry_second); const version = "2018-11-09"; const resource = "b"; const permission = "r"; const ip = ""; // "0.0.0.0/0"; const protocol = ""; // "https"; const canonicalizedResource = `/blob/${this.account_name}/${container}/${blob}`; const rscc = ""; const rscd = `attachment; filename="${filename}"`; const rsce = ""; const rscl = ""; const rsct = ""; const identifier = ""; const snapshot = ""; // シグネチャ (順番は厳守) const stringToSign = [ permission, start, expiry, canonicalizedResource, identifier, ip, protocol, version, resource, snapshot, rscc, // Cache-Control rscd, // Content-Disposition rsce, // Content-Encoding rscl, // Content-Language rsct, // Content-Type ].join("\n"); // HMAC256 const signature = this.getHMAC256(stringToSign); // クエリパラメータ (順番は任意) const sas = [ ["sp", permission], // ["sr", resource], // ["st", start], ["se", expiry], // ["sip", ip], // ["spr", protocol], ["sv", version], ["sig", signature], ["rscd", rscd], ] .map(([k, v]) => `${k}=${encodeURIComponent(v)}`) .join("&"); return sas; } isoDate(now, second = 0) { const date = new Date(now.getTime() + second * 1000); return date.toISOString().substring(0, 19) + "Z"; } getHMAC256(input) { // 1. キー は Buffer (Base64 で デコード) const key = Buffer.from(this.account_key, "base64"); const hmac = crypto.createHmac("sha256", key); // 2. 入力 は Buffer (UTF8 で デコード) const inp = Buffer.from(input, "utf8"); // 3. 出力 は Buffer を Base64 で エンコード const out = hmac.update(inp).digest(); return out.toString("base64"); } // 上を簡単に書き直すと getHMAC256_(input) { const key = Buffer.from(this.account_key, "base64"); return crypto.createHmac("sha256", key).update(input).digest("base64"); } } module.exports = SAS;シグネチャーに container や blob の情報が含まれないから、この SAS トークンは全部のオブジェクトで共通、ということですね。
01/29 追記
ちなみに、az コマンドで同じことをやるには、こんな感じ
ACCOUNT_NAME='ストレージアカウント名' ACCOUNT_KEY='キー' FUTURE_DATE=$(date -v+30M '+%Y-%m-%dT%H:%MZ') az storage account generate-sas \ --permissions r \ --resource-types o \ --services b \ --expiry $FUTURE_DATE \ --account-name $ACCOUNT_NAME \ --account-key $ACCOUNT_KEY