20210129のNode.jsに関する記事は6件です。

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


  1. ファイルをそのままChromiumで開いたら文字化けしなくても、なぜかData URI schemeだと化ける。なぜだ? 

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

Node-REDでSlackのSocket Modeを使ってみた

SlackのSocket Modeを使ってNode-RED上でSlackのイベントを取得するノードを実装したので、その紹介記事になります。

想定読者

やったこと

SlackでSocket ModeがGAされたので、公式のSDKを使ってNode-REDでSocket Modeを使ったEvents API用のノードを実装&公開しました。

開発環境

  • 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の起動とノードの追加

$ 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の実装画面を開きます。

    • 上記のDockerを使った方法では以下のような警告コメントが表示されますが、無視してください。 image.png
  • 画面右上のメニュー展開ボタンをクリックし、パレットの管理->ノードを追加で検索バーにnode-red-contrib-slack-socketを入力し、出てきた項目のノードを追加ボタンをクリックします。(確認のポップアップが表示されるので追加をクリックします)
    image.png

  • ノードのインストールが完了したら(「ノードをパレットに追加しました」というポップが表示されたら)画面左側のノードが羅列されているエリアの下のchatセクションにSlack Listenがあることを確認します。
    image.png

ノードの設定

  • 画面中央にSlack Listenノードを配置(ドラッグ&ドロップ)し、配置したノードをダブルクリックして設定画面を表示します。
    image.png

  • 設定画面の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_mentionmessageなど)を指定します。
      • 複数のイベントを指定する場合はapp_mention,messageのようにカンマ区切りで指定します。
  • 設定画面右上の追加->完了をクリックします。
    キャプチャ.PNG

Slackイベントの取得

  • Slack Listenノードにdebugノードを接続し、画面右上のデプロイボタンをクリックします。
    image.png

  • Slack Listenノードの下にconnectedと表示されることを確認したらSlackからアプリケーション宛てにメンションでメッセージを送ります。

    • connectedと表示されない場合は、Socket Modeが有効になっているか、トークンが有効なものか(Revokeされていないか、app-level tokenかなど)を確認してください。
  • 画面右側にあるデバッグアイコン(虫のアイコン)をクリックし、Slack Listenノードで取得したイベント情報(前項で送られたメッセージの情報)がdebugノードから出力されていることを確認します。
    キャプチャ2.PNG

Slack Listenノードでは取得したイベント情報をそのまま出力しているので、出力内容を事前に知りたい場合はSlackの各Event APIの試用を参照してください。

また、取得したイベント情報の加工などを行う場合は適宜changeノードやfunctionノードなどをつないで処理を実装してください。(以下の例ではアプリケーション宛てのメンションからメッセージ部分のみを取得しています)
image.png

その他

イベントが取得できない場合は以下を確認してみてください。

  • Slack Listenノードの下にconnectedと表示されているかどうか
    • connected以外の場合は設定したトークンが正しいものかどうか確認してください
  • Slackのアプリ管理画面(OAuth & Permissions->Scopes)にてイベント取得のための権限が十分かどうか
  • Slackのアプリ管理画面(Event Subscriptions->Subscribe to bot events)にて取得可能なイベントがちゃんと指定されているかどうか

参考

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

【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.7

nodebrewのインストール

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 setup

Node.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
  • インストール済みバージョンを確認します。
zsh
nodebrew list
# → v12.20.1
  • インストール済みバージョンを有効にします。
zsh
nodebrew 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 
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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個開いているんですよね。

image.png

Puppeteerのドキュメントを見ると、browser.pages()でページ一覧が取れるので、

const browser = await puppeteer.launch({ headless: false, });
const page = (await browser.pages())[0];
await page.goto("https://www.google.com/");

こうすると、もとからあるタブを使ってくれました!

image.png

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

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']
    });

参考

TypeORM - Amazing ORM for TypeScript and JavaScript (ES7, ES6, ES5). Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, WebSQL databases. Works in NodeJS, Browser, Ionic, Cordova and Electron platforms.

typeorm - Is it possible to 'protect' a property and exclude it from select statements - Stack Overflow

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

Node.js で Azure BLOB オブジェクト を 30秒間 だけ Read 可能な SAS トークン を ライブラリを使わないで生成する

tl;dr

SAS トークンの構造を学習するために、ライブラリを使わないで、Node.js で生成しました。
( Java 版は こちら )

公式ドキュメント : サービス SAS を作成する

コード

BLOB オブジェクト を 30秒 だけ Read 可能なURL

main.js
const 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.js
const 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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む