20191222のNode.jsに関する記事は17件です。

迷惑メールを一掃するため,ドメインホワイトリストを作成した

みなさん,迷惑メールで苦労していませんか?

私は苦労しています.年末の大掃除ということで,迷惑メールを一掃してしまいましょう.

状況

キャリアメールに毎日,5〜10 通程度の迷惑メールが着弾します.有料の迷惑メールブロック機能を使用しているのですが,ブロックできるのは 5 割程度で,何通かは私のスマホの着信音を鳴らしてしまいます.

現在個人で利用しているメールアドレスは Gmail のもので,キャリアメールはほぼ使っていません.LINE や Twitter などの SNS 全盛の今(インスタ?TikTok?何それ,おいしいの?),使用するのは家族とのやり取りのみで,他には携帯メールのアドレスでなければ登録できないサービスや,かなり昔に登録したサービスのものがたまに届く程度です.

もはや,受信するメールのほとんどが迷惑メールという状況です.1 年前くらいまではほどんど迷惑メールなんてこなかったのに,なぜほとんど使わないアドレスに急に届くようになったのでしょう.どこかが個人情報流出させたのでしょうか?

対応方針

使っていないメールアドレスとは言っても,全く使っていないわけではないですし,古い知り合いはこのメールアドレスしか知らないということもあります.本来ならメールアドレスを変更するのが筋でしょうが,今では疎遠になっている人たちにアドレス変更を連絡するのも億劫です.何より,アドレス変更したところでどうせまた迷惑メールは届くようになるわけですから,「たとえ迷惑メール業者にメールアドレスを知られても,迷惑メールは届かない」ようにしたいです.

迷惑メールの傾向を調べてみると,適当な文字列で取得したと思われるドメインから送信されています.送信元を偽装しているメールは受信しないように設定しているので,届いているメールに関しては送信元の偽装は行われていません.また,タイトルや本文は,芸能人を思わせるような文脈であったり,何かのサイトの料金を請求するもの,ただ何かの URL が書いてあるだけのものなど,さまざまです.一貫している特徴としては,日本語がおかしいことくらいでしょうか?

ちなみに,「鈴木さんですよね?〇〇(姓)さんから聞きました」という,疎遠になっている知人を装うメールもありました.馬鹿なのかな?私が〇〇だ.

迷惑メールであることを的確に判断するポイントはなさそうですが,正規のドメインを乗っ取るようなことまではしていないので,ホワイトリストで受信するドメインを指定するようにします.やり取りする相手が限られていることと,今後も増えることはほぼないと考えられることから,この対応で少なくとも今は問題ないでしょう.

ホワイトリスト作成

幸運なことに,私はこれまで受信したメールは削除せず,用が済んだメールは「アーカイブ」という名前のフォルダに移動するという使い方をしています.また,迷惑メールは即迷惑メールとして報告する(報告後,ゴミ箱に移動される)か「迷惑メール」というフォルダに放り込むようにしていたため,本来のメールと迷惑メールは完全に分離されています.

このため,「アーカイブ」フォルダから送信元メールアドレスを抽出し,ドメイン部分を取り出すだけでホワイトリストを作れそうです.

いつの間にかキャリアメールも IMAP でアクセスできるのが当たり前になっていますね.NPM の imap というパッケージを使い,指定したフォルダから送信元ドメインを抽出する簡単なスクリプトを書いてみます.

最初に imap と,TypeScript で書きたいので ts-node, typescript, @types/imap をインストールします.

$ yarn add imap
$ yarn add -D ts-node typescript @types/imap

tsconfig.json を書きます.

tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true
  },
  "exclude": ["node_modules"]
}

スクリプト本体である main.ts を書きます.書き捨てのコードなのでちゃんとしたものではないですが,対象のフォルダは決めうちにせず,コマンドライン引数で対象のフォルダを指定するようにします.

main.ts
import util from 'util';
import Imap from 'imap';

const imap = new Imap({
  host: process.env.IMAP_HOST || '',
  port: parseInt(process.env.IMAP_PORT || '993', 10),
  user: process.env.IMAP_USERNAME || '',
  password: process.env.IMAP_PASSWORD || '',
  tls: (process.env.IMAP_TLS || '1') === '1'
});

// util.promisify を使うと,コールバックをとる非同期関数を
// Promise を返す関数に変換することができます.
const openBox = util.promisify(imap.openBox.bind(imap));

const getBody = (message: Imap.ImapMessage) =>
  new Promise<string>((resolve, reject) => {
    let buffer = '';
    message
      .on('body', stream => {
        stream
          .on('data', (chunk: Buffer) => (buffer += chunk.toString('utf8')))
          .on('error', reject);
      })
      .on('end', () => resolve(buffer));
  });

imap
  .once('ready', async () => {
    const domains = new Set();
    const name = process.argv[2];
    // openBox でフォルダを開きます.
    // 第 2 引数が true なら Read Only モードになります.
    // 今回は読み込みしか行わないため,安全のために true にしておきます.
    const box = await openBox(name, true);
    imap.seq
      .fetch(`1:${box.messages.total}`, {
        // bodies には取得する要素を指定するようです.
        // 欲しいのは送信元だけなので,ヘッダーの FROM だけを指定します.
        bodies: 'HEADER.FIELDS (FROM)'
      })
      .on('message', async message => {
        const body = await getBody(message);
        const headers = Imap.parseHeader(body);
        // ドメイン部分だけを取り出し,標準出力に表示します.
        headers.from
          .map(x => x.replace(/.*<(.*)>$/, '$1'))
          .map(x => x.split('@').pop())
          .forEach(domain => {
            // 一度表示したドメインは domains に格納し,
            // 2 回以上表示しないようにします.
            if (domains.has(domain)) return;
            domains.add(domain);
            console.info(domain);
          });
      })
      .on('error', err => console.error(err));
    imap.end();
  })
  .connect();

各種環境変数を設定します.私は direnv を使っているので,パスワード以外は .envrc に環境変数を設定します(使用していない場合は,コマンド実行の前に設定すればいいです).パスワードはどこにも残したくないし,画面にも一切表示されないようにしたいので,いつも read -s で入力するようにしています.

.envrc
layout node

export IMAP_USERNAME=<ユーザー名>
export IMAP_HOST=<IMAP サーバー>
$ echo -n 'Password: ' >&2; read -s p; echo >&2;
Password:
$ IMAP_PASSWORD=$p ts-node main.ts アーカイブ | tee domains.txt
mail.yahoo.co.jp
ezweb.ne.jp
accounts.google.com
k.vodafone.ne.jp
...

これでドメインのリストが得られました.

ここで念のため,生成されたリストが正しいことを確認しておきましょう.私の場合,1 件だけ迷惑メールが「アーカイブ」に紛れ込んでいたため,見覚えのないドメインがありました.

ホワイトリスト設定

今回生成されたホワイトリストには 29 件のドメインがありました.もっと多かったらスクリプト書こうと思ってましたが,この程度なら手動入力でいいでしょう.

ここで一つ注意点があります.私の使用しているキャリアは docomo なのですが,ホワイトリストの設定は,実はドメインではなく,送信元メールアドレスとの後方一致で判定されると書かれています.このまま生成されたドメインを設定すると,「spammixi.com」のようなドメインからのメールを受信してしまいます.これを防ぐためには「@spammixi.com」というように「@」から書いておくとよいです.

image.png
https://www.mydocomo.com/dcm/dfw/web/pub2/useful/sp_mode/domain/spam.html

docomo には上のように「@mixi.jp」を許可し,「mixi.jp」を拒否すると書かれていますが,今回は PC メール全てを拒否にしているので,許可だけを指定します.

入力時に足すこともできますが,面倒なので awk で加工します.

$ cat domains.txt | awk '$0 = "@" $0'
@mail.yahoo.co.jp
@ezweb.ne.jp
@accounts.google.com
@k.vodafone.ne.jp
...

これをひとつひとつコピペして設定していけば,対応完了です.

さらば,迷惑メール.

後始末

もう「迷惑メール」に放り込んでいたメールは不要なので,削除しました.また,有料の迷惑メールブロック機能も今後は無用の長物となってしまうので,少し様子を見て解約しようと思います.

昔話

そういえば大学入学して間もない頃,引っ越して 1, 2 週間くらいで,ある国連機関と同名の謎の組織からダイレクトメールが届いたことがありました.その時点で住所知ってるのって大学を除くと不動産業者,電力会社,ガス会社,水道局だけです.契約直後の個人情報売ったら足がつくのなんて誰でもわかりますし,一体どこが売ったんですかね?

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

[Netlify Formsと連携]AWS Lambda上にGmail自動返信スクリプト(Node.js)をデプロイしAPI化。

Gatsby.jsを使用しNetlifyにデプロイするプロジェクトを進行していたところ、お問い合わせフォームの作成に手間取ったため備忘録。

前提

対象読者は、
・node.jsの使用経験はそこそこある。これは必須です。
・lambdaの存在と癖の強さをある程度把握してる。若しくは、どんな癖でもかかってこいという精神がある人。
・api gatewayとlambdaの連携を行ったことがある。若しくは、自力でどうにかできる自信がある人。
を満たしていれば、全員できると思います。

参考サイト等

詳しい。ここに従えば基本大丈夫(英語です。)
わかりやすい。コード以外はここに従えば大丈夫(日本語です。)

環境セットアップ

1.netlify formsをセットアップ(これが無くてもAPI化できるのですが、APIをテストする検証用という感じです。)

2.こちらにスクリプトを用意したので、ローカルに落とす(コードは下にも貼っておきます。)

3.上の参考サイトから、今回使用する、「clientId」と「clientSecret」と「refreshToken」をメモしといて、2でダウンロードしたスクリプトに記載。

4.ディレクトリ下でzip -r deploy.zip *を実行し、zip化(aws serverless frameworkを使用したい人は各々セットアップしてください)

5.一応lambdaのタイムアウト時間を10秒とかにしておきましょう。保存してください。

6.api gatewayで、lambdaをデプロイ。(後に詳しく説明します。一言で言うと、メソッドをPOSTに設定して、プロキシ統合させれば大丈夫です。)

7.netlify forms notificationにOutgoing webhookトリガーを設定。(後に軽く説明します。ご自身のnetlifyのベースURL/settings/forms#form-notificationsにて設定できます。)

8.以上で完成

という流れになります。3までは単純作業ですので、何か漏れがなければ大丈夫なはずです。5,6などはawsの使い方になりますが、経験がない方には負担の多い作業になると思うので後に説明します。7に関しては、ニッチな設定になるのですが、uiがわかりやすいので大丈夫だと思います。

6.api gatewayで、lambdaをデプロイ。について

僕は以下の構成で実現しています。
スクリーンショット 2019-12-22 19.24.01.png
メソッドの設定は、
スクリーンショット 2019-12-22 19.24.43.png
にて大丈夫です。
もちろん、Lambda関数は、ご自身で作られたlambda関数名を使用してください。

次に使うので、デプロイ先のURLはメモしておいてください。

7.netlify forms notificationにOutgoing webhookトリガーを設定。

まず、NetlifyのSetteingsのページに行ってください。そこの中にある、Formsをクリックしてください。
スクリーンショット 2019-12-22 19.27.12.png
そして、Form notificationsのAdd notificationをクリックして、Outgoing webhookを選択してください。
スクリーンショット 2019-12-22 19.29.30.png

コード

index.js
const nodemailer = require("nodemailer");
const {
    google
} = require("googleapis");
const OAuth2 = google.auth.OAuth2;

const mymail = "送信元のgmailアドレス";
const clientId = "OAuthのclientId";
const clientSecret = "OAuthのclientSecret";
const refreshToken = "//developers.google.com/oauthplaygroundで取得したrefreshToken";

exports.handler = async (event, context, callback) => {
    //netlify formのqueryをparseして自動返信先のメールアドレスを取得.
    const tomail = JSON.parse(event.body).email;

    const start = async () => {
        const oauth2Client = new OAuth2(
            clientId,
            clientSecret,
            "https://developers.google.com/oauthplayground"
        );

        oauth2Client.setCredentials({
            refresh_token: refreshToken
        });
        //アクセストークンに制限時間があるっぽい?ので毎度取得します。
        const accessToken = await oauth2Client.getAccessToken()

        const smtpTransport = nodemailer.createTransport({
            service: "gmail",
            host: 'smtp.gmail.com',
            port: 465,
            secure: true,
            auth: {
                type: "OAuth2",
                user: mymail,
                clientId: clientId,
                clientSecret: clientSecret,
                refreshToken: refreshToken,
                accessToken: accessToken
            }
        });

        const sendMail = async (mailingList) => {
            const mailOptions = {
                from: mymail,
                to: mailingList.join(', '),
                subject: "お問い合わせありがとうございます。",
                text: `${JSON.parse(event.body).email}様、お問い合わせありがとうございます。`
            };
            return await new Promise((resolve, reject) => {
                smtpTransport.sendMail(mailOptions, (error, response) => {
                    error ? console.log(error) : console.log(response);
                    smtpTransport.close();
                    resolve();
                });
            });
        };

        await sendMail([tomail, mymail]);
        return;
    }

    await start().then(() => console.log('done'));


    //aws lambdaでは、responseの形式が決まっているので、適当に記述。
    const response = {
        statusCode: 200,
        headers: {
            "x-custom-header" : "netlify forms test"
        },
        body: JSON.stringify(event)
    };

    return response;
};

まとめ

あまり時間がなく、シンプルなメモとなっていますが、補足リクエスト等あったらお願いします。時間ある際に追記しておきます。

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

node/js向け husky使ってpush前にテストするを共有する

テストや静的コード解析実行前にうっかりpushしてしまう事はないでしょうか?

有意義レビューの為にも事前にテストを実行し、リモートにはテスト通ったコードしか置かないようにしたいですね。

githooksのpre-pushを利用したpush前に必ずテストし、失敗したコードはpushできない状態を作る事ができます。

githooksのpre-pushを利用したpush前テスト

.git/hooks/pre-push に実行したいコマンドを書きます。 pre-pushgit push の前に実行されるフックです。

ほとんどのテストコマンドは、すべて通った場合に終了コード 0 を、失敗した場合に 1 などの0以外を返すようになっています。

0 の場合にのみ git push が実行できるようになります。

$ cp .git/hooks/pre-push.sample .git/hooks/pre-push
$ chmod +x .git/hooks/pre-push
[.git/hooks/pre-push]
#!/bin/sh

yarn test

ただ、この方法ではプロジェクトメンバーへの共有が難しくなります。テンプレ作って pre-push 書き換えてと案内するのは面倒ですね。

husky使ったpush前テスト

husky をinstallして package.json にpush前に実行したいコマンドを書くだけです。(.huskyrc を用意しても良いです)

$ yarn add --dev husky

pre-commit などのhookも用意されています。コミット単位でテストするのは非効率なので pre-push を使います。

package.json
{
  ...
  "husky": {
    "hooks": {
      "pre-push": "yarn test"
    }
  },
  "devDependencies": {
    "husky": "^3.1.0"
  }
}

push時に自動でテストが走り、失敗するとpushできないようになりました。

$ git push

husky > pre-push (node v10.14.1)
yarn run v1.17.3
$ yarn test
...
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
husky > pre-push hook failed (add --no-verify to bypass)

プロジェクトメンバーはパッケージインストール時にpre-pushが書き換わる

clone時のhooksは何も登録されていないsample状態ですが、

$ git clone xxxxx
$ ls .git/hooks
applypatch-msg.sample     fsmonitor-watchman.sample pre-applypatch.sample     pre-push.sample           pre-receive.sample        update.sample
commit-msg.sample         post-update.sample        pre-commit.sample         pre-rebase.sample         prepare-commit-msg.sample

パッケージインストール後に書き換えてくれます。

$ yarn
yarn install v1.21.1
[1/4] ?  Resolving packages...
[2/4] ?  Fetching packages...
[3/4] ?  Linking dependencies...
[4/4] ?  Building fresh packages...
✨  Done in 2.29s.

$ ls .git/hooks
applypatch-msg            fsmonitor-watchman.sample post-merge                post-update.sample        pre-commit                pre-push.sample           pre-receive.sample        sendemail-validate
applypatch-msg.sample     post-applypatch           post-receive              pre-applypatch            pre-commit.sample         pre-rebase                prepare-commit-msg        update
commit-msg                post-checkout             post-rewrite              pre-applypatch.sample     pre-merge-commit          pre-rebase.sample         prepare-commit-msg.sample update.sample
commit-msg.sample         post-commit               post-update               pre-auto-gc               pre-push                  pre-receive               push-to-checkout

まとめ

必ず実行するであろうパッケージインストール後に書き換えてくれるので勝手に共有できるのは良いですね。

githooks管理ツールは他にもpre-commitovercommitなどがありますが、 husky はシンプルで良い感じです。

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

文言のビルド失敗

はじめに

前回

コマンドラインから実行しました
READMEの通りに出来ず、そのレベルに達してないようですが、くじけず進めていきます

実行

usageによるとレンダリングが出来そうなので実行

node ./build/wenyan.js —render ./examples/helloworld.wy

エラー発生
/dev/stdoutにアクセス出来ません

windowsだからありません
リダイレクトしてみる

node ./build/wenyan.js —render ./examples/helloworld.wy > ./renders/helloworld.svg

エラー変わらず

残念

ビルド

それぞれのプラットフォーム用にビルドが必要なのかな。

Makefileを見て実行

cd tools
node ./make_cmdline.js

エラー発生
また/dev/~
デバイスがらみ。潰せない

エラーは出ていたが、wenyan.jsのタイムスタンプが変わったので、サイズが小さくなっているのがきになるが実行してみる。

レンダリングエラーメッセージ変わらず。
windowsでは、無理なのかな。

おわりに

またレンダリングに挑戦します

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

Alexa+lambdaでスマート(じゃない)ホームスキルをつくる

はじめまして@ufoo68です。今回はAWSのアドベントカレンダーにお邪魔させていただきました。

はじめに

このアドベントカレンダーに登録したきっかけは最近re:invent2019に参加した経験からです。普段はあまりAWS関係の記事は書いたりしないのですが、せっかく大きなイベントに参加したのでlambdaについて書いてみよと思った次第です。イベントの発表を聴いて気に入ったサービスについてはこの記事でまとめました。今回はその中のAlexaに挑戦してみようと思い、思い切ってEcho Show 5を購入してみました。

が、到着予定日がまさかの1/17~1/23とかいうことになってしまい(サイバーマンデーでポチったのでこういうこにになったのか?)、Alexa開発がアドベントカレンダーに間に合わないという事態に!!というわけで、今回はシミュレーターとかを使ってスキルを作ってみようかなと思います。まあ、lambda+Alexaでの今後の開発のための練習ということにしておきましょうw

スキルについて

今回つくるのは「スマート(じゃない)ホームスキル」です。本当はスマートホーム的なのやりたかったわけですが、何せ実機がないものですからこういうものを考えました。これは何かというと、スマートホームのフリをして返事だけは一丁前にするスキル、つまり「ライトをつけて」とかいうと「わかりました」と返事だけはするが実際には何もしてくれない、そんなスマートホームもどきのスキルをつくってみようと思います。

スキルの実装

今回はこちらを参考にしてCodeStarを用いてコードを実装やらデプロイを行いました。これを用いると対話モデルの作成とか全部をAWSでやってくれるみたいです。開発は「Hallo World Skill」というサンプルテンプレートを用いて開発しました。ここで公開しています。

まずは、skill.jsonを以下のようにしました。

skill.json
{
  "manifest": {
    "publishingInformation": {
      "locales": {
        "ja-JP": {
          "summary": "スマートじゃない、スマートホームスキル",
          "examplePhrases": [
            "Alexa、スマートじゃないホームを起動して",
            "電気をつけて",
            "電気をけして"
          ],
          "name": "スマート(じゃない)ホーム",
          "description": "スマートじゃない、スマートホームスキル"
        }
      },
      "isAvailableWorldwide": true,
      "testingInstructions": "Sample Testing Instructions.",
      "category": "EDUCATION_AND_REFERENCE",
      "distributionCountries": []
    },
    "apis": {
      "custom": {
      }
    },
    "manifestVersion": "1.0"
  }
}

あとは対話モデルは以下のように設定しました。

interactionModels\custom\ja-JP.json
{
    "interactionModel": {
        "languageModel": {
            "invocationName": "スマートじゃないホーム",
            "intents": [
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": []
                },
                {
                    "name": "LightOnIntent",
                    "slots": [],
                    "samples": [
                        "電気をつけて",
                        "ライトをつけて"
                    ]
                },
                {
                    "name": "LightOffIntent",
                    "slots": [],
                    "samples": [
                        "電気を消して",
                        "ライトを消して"
                    ]

                }
            ],
            "types": []
        }
    }
}

コードもあまりサンプルと変わってませんが、こんな感じです。

lambda\custom\index.js
// This sample demonstrates handling intents from an Alexa skill using the Alexa Skills Kit SDK (v2).
// Please visit https://alexa.design/cookbook for additional examples on implementing slots, dialog management,
// session persistence, api calls, and more.
const Alexa = require('ask-sdk-core');

const LaunchRequestHandler = {
    canHandle(handlerInput) {
      return handlerInput.requestEnvelope.request.type === 'LaunchRequest';
    },
    handle(handlerInput) {
      const speechText = 'スマートじゃないホームへようこそ。';
      return handlerInput.responseBuilder
        .speak(speechText)
        .reprompt(speechText)
        .getResponse();
    }
};
const LightOnIntentHandler = {
    canHandle(handlerInput) {
      return handlerInput.requestEnvelope.request.type === 'IntentRequest'
        && handlerInput.requestEnvelope.request.intent.name === 'LightOnIntent';
    },
    handle(handlerInput) {
      const speechText = 'はい、つけます。';
      return handlerInput.responseBuilder
        .speak(speechText)
        //.reprompt('add a reprompt if you want to keep the session open for the user to respond')
        .getResponse();
    }
};
const LightOffIntentHandler = {
  canHandle(handlerInput) {
    return handlerInput.requestEnvelope.request.type === 'IntentRequest'
      && handlerInput.requestEnvelope.request.intent.name === 'LightOffIntent';
  },
  handle(handlerInput) {
    const speechText = 'はい、消します。';
    return handlerInput.responseBuilder
      .speak(speechText)
      //.reprompt('add a reprompt if you want to keep the session open for the user to respond')
      .getResponse();
  }
};
const CancelAndStopIntentHandler = {
    canHandle(handlerInput) {
      return handlerInput.requestEnvelope.request.type === 'IntentRequest'
        && (handlerInput.requestEnvelope.request.intent.name === 'AMAZON.CancelIntent'
          || handlerInput.requestEnvelope.request.intent.name === 'AMAZON.StopIntent');
    },
   handle(handlerInput) {
      const speechText = 'さようなら!';
      return handlerInput.responseBuilder
        .speak(speechText)
        .getResponse();
    }
};
const SessionEndedRequestHandler = {
    canHandle(handlerInput) {
      return handlerInput.requestEnvelope.request.type === 'SessionEndedRequest';
    },
    handle(handlerInput) {
      // Any cleanup logic goes here.
      return handlerInput.responseBuilder.getResponse();
    }
};

// Generic error handling to capture any syntax or routing errors. If you receive an error
// stating the request handler chain is not found, you have not implemented a handler for
// the intent being invoked or included it in the skill builder below.
const ErrorHandler = {
    canHandle() {
      return true;
    },
    handle(handlerInput, error) {
      console.log(`~~~~ Error handled: ${error.message}`);
      const speechText = `すいません、聞き取れませんでした。`;

      return handlerInput.responseBuilder
        .speak(speechText)
        .reprompt(speechText)
        .getResponse();
    }
};

// This handler acts as the entry point for your skill, routing all request and response
// payloads to the handlers above. Make sure any new handlers or interceptors you've
// defined are included below. The order matters - they're processed top to bottom.
exports.handler = Alexa.SkillBuilders.custom()
  .addRequestHandlers(
    LaunchRequestHandler,
    LightOnIntentHandler,
    LightOffIntentHandler,
    CancelAndStopIntentHandler,
    SessionEndedRequestHandler) // make sure IntentReflectorHandler is last so it doesn't override your custom intent handlers
  .addErrorHandlers(
    ErrorHandler)
  .lambda();

動作のようす

シミュレーターで動かしてこんな感じでちゃんと動いてくれました。

slill.png

まあ、実機もないしスマートホームに対応させてないので返事だけなのですが。。。

さいごに

とりあえず実機が届いたらもう少し色々やってみたいですね。

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

コマンドライン実行

はじめに

前回
サンプルプログラムを実行しました。

ReadMeを読み

コマンドラインで動かしてみます

実行

chmod +xは出来ないのでそのまま実行

./build/wenyan.js

JScriptエラー発生!

JScriptとはwindowsのスクリプト、wshだそうです

node ./build/wenyan.js

usage引数の一覧出て来ました
実行出来そうです。

おわりに

引数にrenderがありました
次はsvgファイル作ってみます

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

文言コマンドライン実行

はじめに

前回
サンプルプログラムを実行しました。

ReadMeを読み

コマンドラインで動かしてみます

実行

chmod +xは出来ないのでそのまま実行

./build/wenyan.js

JScriptエラー発生!

JScriptとはwindowsのスクリプト、wshだそうです

node ./build/wenyan.js

usage引数の一覧出て来ました
実行出来そうです。

おわりに

引数にrenderがありました
次はsvgファイル作ってみます

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

ESLint v6.8.0

v6.7.0 | 次 (未定)

ESLint 6.8.0 がリリースされました。
小さな機能追加とバグ修正が行われています。

このリリースは ESLint 6.x 系 最後のリリースになります。
来月からしばらくの間 7.0.0 のアルファ版・ベータ版がリリースされます。この日本語版リリースノートは 7.0.0 正式リリース (日付未定。3月くらい?) までの間はお休みする予定ですのでご了承ください。

質問やバグ報告等ありましたら、お気軽にこちらまでお寄せください。

? 日本語 Issue 管理リポジトリ
? 日本語サポート チャット
? 本家リポジトリ
? 本家サポート チャット

? 本体への機能追加

.eslintrc.cjs をサポート

? RFC043, #12321

設定ファイルの拡張子として、新たに .cjs がサポートされました。

type:"module"を持つ package.json の下にある .eslintrc.js が ES Module として解釈され、ESLint はそれを扱うことができないためです。

--no-error-on-unmatched-pattern CLI オプション

? #12377

ESLint はリントが成功すると何も出力しません。そのままではコマンドを typo していて対象ファイルが1つも無い場合とリント成功とを区別できないため、リント対象が1つも無い場合にエラーを表示しています。

例1
> eslint lob

Oops! Something went wrong! :(

ESLint: 6.8.0.

No files matching the pattern "lob" were found.
Please check for typing mistakes in the pattern.

>

このエラーを抑制するオプションが追加されました。

例2
> eslint lob --no-error-on-unmatched-pattern
>

eslint:recommended 設定から require-atomic-updates ルールが削除された

? #12599

eslint:recommended設定から require-atomic-updates ルールが削除されました。誤検出が多かったためです。

設定ファイルが $schema プロパティを許容するようになった

? #12612

設定ファイル (特に .eslintrc.json) が $schema プロパティを持っていてもエラーを投げなくなりました。一部のエディタでは、$schema プロパティを適切に指定することで入力補完やエラーチェックが働くようになります。

? 新しいルール

特になし。

? オプションが追加されたルール

特になし。

✒️ eslint --fix をサポートしたルール

特になし。

⚠️ 非推奨になったルール

特になし。

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

【Cypress】~7選~ CypressでE2Eテストするときはまずこれ読んどけって話

この記事はAteam Hikkoshi Samurai Inc. & Ateam Connect Inc.(エイチーム引越し侍、エイチームコネクト) Advent Calendar 2019 24日目の記事です。

本日はエイチーム引越し侍中途入社3年目、自称爆速Webエンジニアの加藤が担当します!

みなさんE2Eテスト書いてますか??
引越し侍では10年以上続くレガシーシステムにテストが充実していないというカオスな現場があります。

とりあえずこのプロジェクトでリリースしたらここのCVチェックをしようという
力技で確認し続ける日々…

くそぅ!新規サービスではテストを書いているのに!
レガシーシステムにはテストも入れられないのか!

E2Eテスト導入への障壁

一度うちのエンジニアの神様がE2Eを導入しかけたんですが、
ABテストとかに対応するのめんどくさいとのことでなかなか拡充していかなかったんですよね
その姿を見た僕は「めんどくさいんだろうなぁ」と指をくわえて見てましたが、
中途3年目、気づけば社内エンジニア社歴はTOP7に、神セブンってやつですよ。
しかも僕より上の人たちはマネージメントだったり日々の業務に追われて中々動き出せなさそう。

思い立つ

このテスト文化がなじんできた今声を大にして言うしかない!

唐突な目次

ほぼこれで行ける
js 呼び出せない…
ラベル貼っている要素へのアクセス
ビデオ録画やめる
jsエラーに敏感
IP問題
フロントエンジニアとの協力体制が必要

とりあえず書いてみた

ほぼこれでいける

cy.visit(url)   # urlにアクセス
cy.get(セレクタ) # html上の要素を取得
cy.get(セレクタ).click() # 要素をクリック
cy.get(セレクタ).type(文字列) # 要素に文字列を挿入(テキストボックスとか)
cy.get(セレクタ).select(文字列) # セレクトボックスの中身を選択(valueでもテキスト部でもどちらでも指定可)
cy.wait(ミリ秒) # 指定したミリ秒待つ
cy.go('back') # ブラウザバック
cy.go('forward') # 進む

まだ始めたばっかでわかんないけどこれで大体いけんじゃね?
※これもいるよ!ってコメントで教えてもらったら随時追加していきます!

js呼び出せない

Cypress入れようと思った時にNode.jsなんだからそのページで使っているjsとかまで使えんじゃね?
便利ー!と思ってたんですけど呼び出せない…そんな関数ないよってシンプルに怒られます。
まだ読み込み終わってないんかなとwaitしてもダメ。
だれかやれる方法あったらコメントで教えてください。
速攻で記事修正します。

ラベル貼っている要素へのアクセス

なんかラベルを張られている要素にアクセスしようとすると怒られる模様

CypressError: Timed out retrying: cy.type() failed because this element:

<input type="text" name="request[current_zip_code]" value="" id="request_current_zip_code">

is being covered by another element:

<label for="request_current_zip_code">例)1500001</label>

そんなときはforceを入れる

.select(文字列, {force: true})
.type(文字列, {force: true})

ビデオ録画やめる

なんかテストのたびに動画撮ってる
こっちは勉強中なんだよ!さっさと何回も回したいんだよ!と思ったら
cypress.jsonに下記を追加

 "video": false

jsエラーに敏感

1例ですが、formタグに使っていなく、宣言自体も読み込まれなくなった関数がありまして、
ボタンにonclick属性で指定されている関数でsubmitしている感じだったんですが、
chromeだと遷移できるのに、Cypressちゃんはエラーで停止しました。

# 使っていないsubmitFuncまで発火しエラーにある
<form name="form1" enctype="multipart/form-data" method="get" action="/request/base"  onsubmit="submitFunc(this)">

# onclick
<a onClick="submitForm(document.form1); return false;">

IP問題

弊社ではテストのために、社内IPからのアクセスの時にはテストデータとする処理があります。
本番でテストするなら上記の方法以外でテストデータとする処理を何かしらいれないとなぁ…

フロントエンジニアとの協力体制が必要

<a class="btn-entry" onClick="submitForm(document.form1); return false;">

このリンクをクリックしたいけどclassで指定したくない名前だな…
id追加してとりあえず対応するけど、このid使ってなくね?消しちゃえってなるとやだし、
わかりやすいタグつけたほうがいいだろうなぁ

<a id="cypress_submit_button_1 " class="btn-entry" onClick="submitForm(document.form1); return false;">

こんな感じだとわかるかなぁ
要相談

とりあえず書いてみた

describe('PC 一括CV', () => {
  context('正常な依頼', () => {
    it('正常にCVできること',() => {
      const samurai_top = 'https://hikkoshizamurai.jp.test.hoge.com/'
      const user = {
        currentZipCode: '1010051',
        currentStreetName: '2-11',
        currentBuildingType: 'マンション',
        newZipCode: '1010051',
        lastName: '加藤' + Math.random().toString(32).substring(7),
        firstName: '太郎',
        lastNameKana: 'カトウ',
        firstNameKana: 'タロウ',
        tel: '090' + Math.floor(Math.random() * 100000000), // 重複データにならないようにランダム文字列生成
        email: 'kato.shogo+' + Math.random().toString(32).substring(7) + '@a-tm.co.jp' // 重複データにならないようにランダム文字列生成
      }
      cy.log("トップにアクセスします")
      cy.visit(samurai_top)
      cy.log("フォームに遷移します")
      cy.get('#mainForm-move-entry')
        .click()

      // base
      cy.log("住所情報を入力します")
      cy.get('#request_current_zip_code')
        .type(user.currentZipCode, {force: true})
      cy.get('#request_current_street_name')
        .type(user.currentStreetName, {force: true})
      cy.get('#request_current_building_type')
        .select(user.currentBuildingType, {force: true})
      cy.get('#request_new_zip_code')
        .type(user.newZipCode, {force: true})
      cy.wait(2000)
      cy.get('input[name="submit"]')
        .click()

      // personal
      cy.log("個人情報を入力します")
      cy.get('#request_user_name_last_name')
        .type(user.lastName, {force: true})
      cy.get('#request_user_name_first_name')
        .type(user.firstName, {force: true})
      cy.get('#request_user_name_kana_last_name_kana')
        .type(user.lastNameKana, {force: true})
      cy.get('#request_user_name_kana_first_name_kana')
        .type(user.firstNameKana, {force: true})
      cy.get('#request_whole_tel_no')
        .type(user.tel, {force: true})
      cy.get('#request_email_address')
        .type(user.email, {force: true})
      cy.get('#request_advertise_type_2')
        .click()

      // packages
      cy.log("荷物情報を入力します")
      cy.get('input[name="submit"]')
        .click()

      // confirm
      cy.log("確認画面です")
      cy.get('input[name="submit"]')
        .click()
    })
  })
})

CVできたぜ

いかがでしたでしょうか。
CypressでE2Eテスト書く時の初動になれればと思います。

Ateam Hikkoshi Samurai Inc. & Ateam Connect Inc.(エイチーム引越し侍、エイチームコネクト) Advent Calendar 2019
明日は@zwirky が年末にバグバッシュを技術開発部のメンバー全員でやったことを記事にしてくれます!
お楽しみに!

追伸

株式会社エイチーム引越し侍では、一緒にサイト改善をしてくれるWebエンジニアを募集しています。エイチームグループのエンジニアとして働きたい!という方は是非、以下のリンクから応募してください。
皆様からのご応募、お待ちしております!!

エイチームグループ採用サイト(Web開発エンジニア職)

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

ディススレイがOFFになったらRaspberryPi経由でTV電源をOFFにしたい

BRAVIA X9500Gを最近買ってHDMI経由でPCと接続しているのですが、このテレビにはPCからの入力がOFFになったときに、自動的にTV電源がOFFになる機能がありませんでした1
そこで、2年前くらいに買って転がっていたRaspberry Pi Zero WHを使ってRaspberry Pi経由で電源OFFを試してみました。

方針

TV操作について

HDMI-CECを使ってTVを操作します。
残念ながらPCについているHDMIからではHDMI-CEC信号を送れないことが多いらしく、対象のPC(NVIDIAグラボ)でも送れませんでした。
Raspberry PiはHDMI-CECを使えるようなので、HDMIでTVに接続してcec-clientを使ってTVを操作します。

cec-clientをインストールしておけば、echo "standby 0" | cec-client -s -d 1コマンドでTV電源をOFFにできます。

モニタOFF検知について

PCはWindows 10なのですが、簡単に検知する方法はなさそうでした。
Win32 APIのRegisterPowerSettingNotificationを使うと通知されるようになるWM_POWERBROADCASTメッセージから情報を取得します。

モニタOFFを検知したら、HTTPでサーバ(Raspberry Pi)にTV電源OFFを指示します。
なお、クライアント側・サーバ側ともにNode.jsを使っていきます。

環境

Raspberry Pi バージョン
OS Raspbian GNU/Linux 9.4 (stretch)
Node.js v8.11.1
cec-client (libCEC) 4.0.2

Raspberry PiはIPアドレスを192.168.1.100で固定してHTTP通信できるところまで設定済

PC バージョン
OS Windows 10 Pro 1909
Node.js v10.17.0

PC側で使用するライブラリは以下のとおり

dependencies
    "console-stamp": "^0.2.9",
    "ref-array": "^1.2.0",
    "ref-struct-di": "^1.1.0",
    "unirest": "^0.6.0",
    "win32-api": "^6.2.0"

Raspberry Pi側のソース

サーバ側は、リクエストBodyの内容をcec-clientに入力するだけです。

server.js
const http = require('http');
const { execSync } = require('child_process');

http.createServer((req, res) => {
    let body = [];
    req.on('error', (err) => {
        console.error(err);
    }).on('data', (chunk) => {
        body.push(chunk);
    }).on('end', () => {
        body = Buffer.concat(body).toString();
        console.log(`body: ${body}`);

        const command = `echo "${body}" | cec-client -s -d 1`;
        const result = execSync(command).toString();
        console.log(`result: ${result}`);

        res.writeHead(200, { 'Content-Type': 'text/plain' });
        res.end(result);
    });
}).listen(8080);

console.log('server started');
start.sh
#!/bin/sh

/opt/nodejs/bin/node /opt/cec_server/server.js &

起動時に動いてほしいので、cronで@rebootを指定します。
パイプでloggerにつないで、ログが/var/log/messagesに出るようにしています。

cron設定
@reboot /opt/cec_server/start.sh | logger -t cec_server

PC側のソース

クライアント側は少し大変で、メッセージ受信用のWindowとコールバック関数を作っていく必要があります。
また、受信したメッセージ内のPointerのアドレスをStructに変換していく仕組みも必要です。

まずは、struct.jsでGUIDPOWERBROADCAST_SETTINGのStructの定義と、ffiを使ってRegisterPowerSettingNotification関数を呼び出すための仕組みを作っていきます。

GUIDの値はGUID_CONSOLE_DISPLAY_STATEとGUID_MONITOR_POWER_ONのどちらでも使えますが、"New applications should use GUID_CONSOLE_DISPLAY_STATE"とのことなので、こっちを使っています。

struct.js
const ffi = require('ffi');
const ref = require('ref');
const Struct = require('ref-struct-di')(ref);
const ArrayType = require('ref-array');

const GUID = Struct({
    Data1: ref.types.long,
    Data2: ref.types.short,
    Data3: ref.types.short,
    Data4: ArrayType(ref.types.uchar, 8)
});
const LPCGUID = ref.refType(GUID);

function translateGUID(strGUID) {
    const pattern = /(\w+)-(\w+)-(\w+)-(\w{2})(\w{2})-(\w{2})(\w{2})(\w{2})(\w{2})(\w{2})(\w{2})/;
    const [d1, d2, d3, ...d4] = pattern
        .exec(strGUID)
        .slice(1)
        .map(v => parseInt(v, 16));
    return GUID({ Data1: d1, Data2: d2, Data3: d3, Data4: d4 });
}
function equalsGUID(guid1, guid2) {
    return JSON.stringify(guid1) == JSON.stringify(guid2);
}

const GUID_CONSOLE_DISPLAY_STATE = translateGUID('6FE69556-704A-47A0-8F24-C28D936FDA47');
// export const GUID_MONITOR_POWER_ON = translateGUID('02731015-4510-4526-99E6-E5A17EBD1AEA');

const POWERBROADCAST_SETTING = Struct({
    PowerSetting: GUID,
    DataLength: ref.types.ulong,
    Data: ArrayType(ref.types.uchar, 1)
});

const RegisterPowerSettingNotification = ffi.Library('user32.dll', {
    RegisterPowerSettingNotification: ['long', ['long', LPCGUID, 'long']]
}).RegisterPowerSettingNotification;

module.exports = {
    GUID_CONSOLE_DISPLAY_STATE: GUID_CONSOLE_DISPLAY_STATE,
    POWERBROADCAST_SETTING: POWERBROADCAST_SETTING,
    RegisterPowerSettingNotification: RegisterPowerSettingNotification,
    equalsGUID: equalsGUID
};

detect_poweroff.jsでは、createWindowでメッセージ受信用の非表示のウィンドウを作って、registerNotificationで通知の登録をしていきます。
Windowを作る際にも指定したWndProcでコールバック(メッセージ)を待ち受けます。

WndProcの中では、対象のメッセージのlParamPOWERBROADCAST_SETTINGへのポインタのアドレスが入っているので、bufferAtAddressを使ってアドレス数値からBufferを構築しています。
lParamがNumber型になっていて、整数を53ビットまでしか扱えないのに問題ないか気になりましたが、Windows 10ではサポートする仮想アドレス空間は48ビットのため大丈夫なようです2

外から呼び出されるstartMessageLoopでは、メッセージをWndProcに転送するメッセージループを開始します。

detect_poweroff.js
const { U, K, DTypes, DStruct, Config } = require('win32-api');
const [W, DS] = [DTypes, DStruct];
const ffi = require('ffi');
const ref = require('ref');
const Struct = require('ref-struct-di')(ref);
const {
    GUID_CONSOLE_DISPLAY_STATE,
    POWERBROADCAST_SETTING,
    RegisterPowerSettingNotification,
    equalsGUID
} = require('./struct.js');

const WM_POWERBROADCAST = 536;
const PBT_POWERSETTINGCHANGE = 32787;

const user32 = U.load();
const kernel32 = K.load();

var callback_f = () => {};
function registerCallback(f) {
    callback_f = f;
}

function registerNotification(hWnd, guid) {
    const hWndAddr = ref.address(hWnd);
    const hPowerNotify = RegisterPowerSettingNotification(hWndAddr, guid.ref(), 0);
    console.log('register result:', hPowerNotify);
    const error = kernel32.GetLastError();
    console.log('register error:', error);
}

function bufferAtAddress(address, bufferType) {
    if (address > Number.MAX_SAFE_INTEGER) {
        throw new Error('Address too high!');
    }
    const buff = Buffer.alloc(8);
    buff.writeUInt32LE(address % 0x100000000, 0);
    buff.writeUInt32LE(Math.trunc(address / 0x100000000), 4);

    buff.type = bufferType;
    return ref.deref(buff);
}

const WndProc = ffi.Callback('uint32', [W.HWND, W.UINT, W.WPARAM, W.LPARAM], (hwnd, uMsg, wParam, lParam) => {
    console.log('WndProc callback:', uMsg, wParam, lParam);

    if (uMsg == WM_POWERBROADCAST && wParam == PBT_POWERSETTINGCHANGE) {
        const buf = bufferAtAddress(lParam, ref.refType(POWERBROADCAST_SETTING));
        const setting = buf.deref();
        const state = setting.Data[0];
        console.info('power state:', state);

        if (equalsGUID(setting.PowerSetting, GUID_CONSOLE_DISPLAY_STATE)) {
            console.log('GUID_CONSOLE_DISPLAY_STATE');
            callback_f(state);
        }
    }
    return 0;
});

function createWindow(clazzName) {
    const className = Buffer.from(clazzName + '\0', 'ucs-2');
    const windowName = Buffer.from(clazzName + '_Window\0', 'ucs-2');

    const hInstance = ref.alloc(W.HINSTANCE);
    kernel32.GetModuleHandleExW(0, null, hInstance);

    const wClass = new Struct(DS.WNDCLASSEX)();
    wClass.cbSize = Config._WIN64 ? 80 : 48; // x86 = 48, x64=80
    wClass.style = 0;
    wClass.lpfnWndProc = WndProc;
    wClass.cbClsExtra = 0;
    wClass.cbWndExtra = 0;
    wClass.hInstance = hInstance;
    wClass.hIcon = ref.NULL;
    wClass.hCursor = ref.NULL;
    wClass.hbrBackground = ref.NULL;
    wClass.lpszMenuName = ref.NULL;
    wClass.lpszClassName = className;
    wClass.hIconSm = ref.NULL;

    if (!user32.RegisterClassExW(wClass.ref())) {
        throw new Error('Error registering class');
    }
    const hWnd = user32.CreateWindowExW(0, className, windowName, 0, 0, 0, 0, 0, null, null, hInstance, null);
    registerNotification(hWnd, GUID_CONSOLE_DISPLAY_STATE);
}

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

async function startMessageLoop() {
    const msg = new Struct(DS.MSG)();
    while (true) {
        if (user32.PeekMessageW(msg.ref(), null, 0, 0, 1)) {
            user32.TranslateMessageEx(msg.ref());
            user32.DispatchMessageW(msg.ref());
        }
        await sleep(100);
    }
}

createWindow('Node.js_idle-poweroff');

module.exports = {
    registerCallback: registerCallback,
    startMessageLoop: startMessageLoop
}

send_poweroff.jsでは、モニタON/OFFを検知した際に呼ばれるコールバックを登録して、メッセージループを開始します。
コールバックが呼ばれたら、HTTPでHDMI-CECコマンドを送信します。

send_poweroff.js
require('console-stamp')(console, 'yyyy-mm-dd HH:MM:ss');
const unirest = require('unirest');
const { registerCallback, startMessageLoop } = require('./detect_poweroff.js');

const DISPLAY_OFF = 0;
const DISPLAY_ON = 1;

const url = 'http://192.168.1.100:8080';

function sendCommand(command) {
    console.info('send command:', command);
    unirest
        .post(url)
        .send(command)
        .end(res => {
            console.log('response body:', res.body);
        });
}

registerCallback(state => {
    if (state == DISPLAY_OFF) {
        sendCommand('standby 0');
    } else if (state == DISPLAY_ON) {
        sendCommand('on 0');
    }
});
startMessageLoop();
start_idle_poweroff.bat
cd %~dp0
node send_poweroff.js

実行結果は以下のとおり。
PC側のON/OFFに合わせてTV電源が連動するようになりました。

実行結果
[2019-12-22 10:23:04] [LOG]    WndProc callback: 36 0 267690495392
[2019-12-22 10:23:04] [LOG]    WndProc callback: 129 0 267690495296
[2019-12-22 10:23:04] [LOG]    WndProc callback: 131 0 267690495424
[2019-12-22 10:23:04] [LOG]    WndProc callback: 1 0 267690495296
[2019-12-22 10:23:04] [LOG]    register result: -1532784720
[2019-12-22 10:23:04] [LOG]    register error: 0
[2019-12-22 10:23:04] [LOG]    WndProc callback: 536 32787 2382176198256
[2019-12-22 10:23:04] [INFO]   power state: 1
[2019-12-22 10:23:04] [LOG]    GUID_CONSOLE_DISPLAY_STATE
[2019-12-22 10:23:04] [INFO]   send command: on 0
[2019-12-22 10:23:04] [LOG]    WndProc callback: 799 1 0
[2019-12-22 10:23:08] [LOG]    response body: opening a connection to the CEC adapter...

[2019-12-22 10:29:48] [LOG]    WndProc callback: 536 32787 2382174664240
[2019-12-22 10:29:48] [INFO]   power state: 0
[2019-12-22 10:29:48] [LOG]    GUID_CONSOLE_DISPLAY_STATE
[2019-12-22 10:29:48] [INFO]   send command: standby 0
[2019-12-22 10:29:52] [LOG]    WndProc callback: 537 7 0
[2019-12-22 10:29:52] [LOG]    response body: opening a connection to the CEC adapter...

[2019-12-22 10:31:49] [LOG]    WndProc callback: 536 32787 2382147455920
[2019-12-22 10:31:49] [INFO]   power state: 1
[2019-12-22 10:31:49] [LOG]    GUID_CONSOLE_DISPLAY_STATE
[2019-12-22 10:31:49] [INFO]   send command: on 0
[2019-12-22 10:31:52] [LOG]    response body: opening a connection to the CEC adapter...

感想

Node.jsでRegisterPowerSettingNotificationを使うサンプルが見当たらなかったので作ってみましたが、予想以上に大変でした。
特にrefの使い方は試行錯誤が必要で、hWndをログ出力しようとするとログが出ずにプログラムが落ちるなど、解析が難しい場面が多くありました。
また、lParamの型は W.LPARAMのところをW.PINT64などに変えるとBuffer型になるのですが、そこからうまくStructに変換ができませんでした。

はじめはPCのアイドル時間を(これもWin32 API経由で)取得して電源OFFをするようにしていました。その場合はPC側のコードは30行程度で済みます。
しかし、それだと動画を見ている最中に電源がOFFになってしまうという課題がありました。

参考にしたページ

https://stackoverflow.com/questions/48720924/python-3-detect-monitor-power-state-in-windows
https://github.com/waitingsong/node-win32-api/blob/master/demo/create_window.ts
https://github.com/TooTallNate/ref/issues/96


  1. 「無操作電源オフ」の設定はあるものの、これはテレビのリモコンを操作しなかった場合にOFFになる設定のため、使えませんでした。サポートにも問い合わせましたが機能はないとのこと 

  2. アドレス数値をBufferに変換する方法は他に見つかりませんでした 

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

Slack Appを無料で運用する(Netlify Functions)

背景

前回はSlack Advent Calendar 2019 6日目として、Microsoft TeamsとSlackの比較記事を投稿しました。
https://qiita.com/hide2018/items/d410b16d75d190ae4271

今度はSlackに焦点を当て、ワークフロービルダーについて書こうとしました。
しかし無料プランでは使えませんでした:frowning2:残念
https://qiita.com/kazushi_iizuka/items/5cd9c3bb0bde9803cac0

そこで無料で使えるSlack Appについて書きます。
以下の記事が良いなと思ったので自分でも作ってみます。

(記事1)
社内slackにVIPチャンネルを作った話
https://qiita.com/peisuke/items/80984db8b47cd8243019

ただAWS Lambdaは無料枠があるものの、クレジットカード登録が必要です。
https://aws.amazon.com/jp/aws-jp-faq/prospects/

アカウント作成およびご利用開始にあたっては、本人確認および無料範囲を超えたご利用に対する料金のご請求といった点から、クレジットカード登録は必須となります。

無料かつクレジットカード不要で開発したいので、今回は代わりにNetlify Functionsを使います。

(記事2)
【入門】Netlify Functionsコトハジメ
https://qiita.com/Sr_Bangs/items/7867853f5e71bd4ada56

Netlify Functionsの中身はLambdaです。
ただ、記事1はPythonで書いてますが、Netlify FunctionsはJavaScriptしか動きません。
そのためNode.jsで書き直します。

手順

vipチャンネルとvipアプリを作成する

vipチャンネルを作ります。
image.png

自分の代わりに匿名でメッセージを投稿してくれるアプリを作ります。
Slack Apiのページでアプリを作ります。
https://api.slack.com/apps

image.png

アプリの名前と、導入するSlackのワークスペースを入力します。

image.png

権限を設定します。

image.png

image.png

Step 1: Select bot token scopesで以下にチェックを付けます。

  • chat:write (メッセージを投稿できる)
  • commands (スラッシュコマンドを使える)

以降は、特に設定するところはありません。
画面に沿って完了させます。

次にワークスペースにアプリをインストールします。

image.png

許可します。

image.png

後で使うのでトークンをメモっておきます。

image.png

Appのページを見るとvipアプリが追加されています。

image.png

vipチャンネルでvipアプリを追加します。

image.png

追加されました。

image.png

メッセージを投稿できるようにする

全体の流れは以下です。

  1. Slackでスラッシュコマンド/2chでメッセージを打つ。例えば、/2ch ぬるぽと打つ。
  2. Netlify FunctionsのAPIにぬるぽがPostされる。
  3. Netlify Functionsのスクリプトでぬるぽがvipチャンネルに投稿される。

この章では3ができるようにします。
1,2は次の章で設定します。

Netlify Functionsの使い方は記事2を参照ください。
(申し訳ございません、執筆に力尽きました…)

ソースは以下です。
https://github.com/hideboh/qiita_slack_bot

Slackに投稿するスクリプトは以下です。

resources/api/slack_vip.js
const { WebClient } = require('@slack/web-api');
const token = process.env.TOKEN;
const web = new WebClient(token);
const querystring = require('querystring');

exports.handler = async (event) => {
    const body = querystring.parse(event.body);
    const result = await web.chat.postMessage({
        text: body.text,
        channel: '#vip',
    });

    return {
        statusCode: 200,
        body: 'vipに投稿したお( ^ω^)',
    };
};

スラッシュコマンドを設定する

Slack Apiのページでスラッシュコマンドを設定します。

image.png

以下のように設定して保存します。
Request URLは、Netlify FunctionsのAPIのEndpointを入力してください。

image.png

/2と打つと/2chコマンドが認識されていることが分かります。

image.png

なお、公開チャンネルでこのコマンドを打ってしまうと、typingって出て匿名性が失われます。
そのため自分へのダイレクトメッセージで打ってみます。

image.png

vipチャンネルに匿名で投稿されました!

image.png

アイコンを変える

このままだと味気ないのでアイコンを変えます。

image.png

登録します。

image.png

登録できる画像は意外と厳しく、512px~2000pxの正方形でないといけません。
このサイズの画像が見つからなかったので、こちらのサイトでリサイズしました。
https://www.image440.com/

それっぽくなりました。

image.png

まとめ

これでSlackで2ch気分を味合えます!
IDやコテハンをつけるところまでいけませんでしたが、全体の流れは分かりました。
機能拡張も頑張ればできそうです。

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

Node.jsで永続化Cookieに対応する

発端

REST 使うコンソールアプリを TypeScript で書きたかったので、 Node.js と axios を使ってコードを書いていたのですが、ログイン操作や読み取り操作など毎回プロセスを起動して落とす設計にしたところ、 Permanent Cookie がプロセス終了とともに消滅してしまう状態でした。

Permanent Cookie?

mdn によると セッション Cookie持続的 Cookie の二つがあると紹介しており、前者は有効期間の指定がないのでクライアントを終了すると消えるとあります(ECのカートのような一時的なセッションで使われる感じですかね)。そして後者は設定された有効期間までは存続すると読み取れそうです(ログイン用途で使えそう)。
実際試したところ、ブラウザや iOS/macOSのAPI はプロセス再起動しても自動でCookieを付与してくれたのですが、Node.js + axios ではやってくれませんでした。どうやらディスクに保存するような別の仕組みが必要そうです。

対応方法

最悪デーモンプロセスが必要かもな、、と思いつつ調べたところ tough-cookie という強そうな名前のライブラリを見つけることができました。これ自体はディスクに保存しないのですが、ドキュメントを読んでみると Store API という仕組みがあることに気がつきます。

The default is MemoryCookieStore which can be found in the lib/memstore.js file.

とても気になる文言です。 lib/ に FileCookieStore のような実装があればそれに切り替えるだけで実現できそうですが残念ながらありませんでした。

追加のライブラリを検索

もしかしたら誰かが実現しているかもと思い、次にGoogleで "tough-cookie file" と検索しようとしたところ、 tough-cookie-filestore がサジェストされました。おお、これは試してみたい。

コード

まず axios と tough-cookie をどう連携させるかという問題に直面するわけですが、以前 axios が同一プロセス内でも cookie を付与してリクエストを送ってくれないという別の問題に対処すべく こちらの記事 を読んでいたので、 cookiejar を使えばうまくいくことを運良く知っていました。

なので axios cookiejar で検索してみたわけですが今度は同じゆめみの @Ancient_Scapes さんの記事に使い方が書いてあったので時間短縮できました(助かる〜): axiosでrequestと同じようにjarを使えるようにする方法

というわけで filestore のインスタンス作ってセットするだけでした。簡単。

import axios from 'axios';
import axiosCookiejarSupport from 'axios-cookiejar-support';
import { CookieJar } from 'tough-cookie';
import FileCookieStore from 'tough-cookie-filestore';

export const createClient = (baseURL: string, cookiePath: string) => {
  const filestore = new FileCookieStore(cookiePath);
  const jar = new CookieJar(filestore);
  axiosCookiejarSupport(axios);
  return axios.create({
    withCredentials: true,
    jar,
    baseURL
  });
};

TypeScriptでもバッチリでした。素晴らし。

注意事項としては、上記のコードではデフォルトで cookiePath に空のファイルがないとエラーになります。writeFileしておきましょう。

余談

cookiejar ってどういう意味なんだろと思いググってみたところ、 weblioのページ が引っ掛かったのですが、

クッキーが保存される瓶(時々お金が隠される)

という意味らしいです。時々お金が隠されるというところがとても気になります、海外のアニメ作品とかかな :thinking:

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

思いもよらないものをnpm publishしてしまった話(前任者の顔写真など)

スクリーンショット 2019-12-23 0.06.32.png
俺「すっげぇいい名前のライブラリ思いついた!!」

俺「npm あるかな?あるかな?」

(カタカタ)

俺「なかった!よっしゃ一番乗りや!!!今すぐ作らないと!!!」

俺「npm init enter enter enter enter npm publish うおおおおおおおいっけぇぇぇ!!!!!!!」

(カタカタカタカタカタカタカタカタッターン)

俺「ミ゜ッ!」

ーーー

おはようございます。本番環境でやらかしちゃった人 Advent Calendar 2019の 23 日目を担当する、@sandessOjisanです。この記事では 思いもよらないものを npm publish したお話 を紹介します。

僕は あるとき 色々な会社の仕事を請け負っていた時期があり、そのときの無邪気なnpm publishによって、良くないことをしてしまったというお話しです。このコマンドを叩くといきなり本番環境に上がるので、1ミスが命取りです。僕が何をしでかしたか、ここでだいたい想像ついた方もいらっしゃると思いますが、その方はニヤニヤしながらお楽しみください。あと、ところどころ誤魔化しているのは意図的であり、また現在の所属でのやらかしではありません。

では、やらかしを 2 つと、その対策について紹介します。

ツールを publish したら、Collaborators に前任者の顔写真が上がった話

npm のざっとした説明

普段 JS を書かない方も読まれていると思いますので、僕がやらかした npm publish が何かについて説明します。npm は Node Package Manager の略で、主に JS プロジェクト のための、パッケージ・ライブラリ管理システムです。 ライブラリは npm registry に登録することで、配布や共有が可能になります。そして、npm publish というコマンドが npm registry に登録するコマンドです。

npm に登録すると、このようなページが作られます。(react の npm ページ)

react.png

このとき、開発者は collaborators として登録されます。

collaborators.png

普通、ここには自分で作った ライブラリ であれば自分のアイコンが表示されるはずです。しかし僕がライブラリを作った時、ここに僕のアイコンはありませんでした。

自分が作ったライブラリの collaborators 欄に前任者の顔写真が載った話

あるとき、某社の某チームにて、ちょっとしたライブラリを作っていました。それはその会社のいろんなプロジェクトから呼び出されるものであり、ライブラリ(node_module)として開発されていました。そして、どうしてか npm 経由で配布していました。1

僕はそのツールをメンテナンスする仕事をしていたのですが、どうしても必要なモジュールがあったので、それを別ライブラリとして開発、publish しようとしました2。このときの publish の方法は、

  1. 責任者から npm publish 用の共用アカウントの id/password を教えてもらい
  2. それをターミナルに打ち込んで手元から publish する

というものでした。(※そもそもその運用がヤバいだろというツッコミはお控えください。あとこの運用しているところ何個か知っているので、別に珍しい話でもないです。ただ、良くない運用です。)

早速 publish しようと id/password を打ち込んでログインし、npm publishを行いました。どんな風にページができるかたまたま気になったので npm ページで成果物を確認しに行きました。なんとページを開くとそこには前任者の顔写真がありました。

pose_dance_ukareru_man.png

このときプロジェクトの成果物を前任者の顔写真に紐づけてしまったので、

  • 会社の看板を背負って、人の顔写真をあげてしまった
  • 前任者の顔写真をその名の通りグローバルなインターネットに公開してしまった

というやらかしをしたことに気づき、すごい冷や汗が出てきました。が、責任者や前任者にそのことを報告するとゲラゲラ笑われたので結果的には大丈夫でした。そこで余裕をもって原因を解明してみました。

何がおきたのか

最初は package.json を疑いました。何かをコピペしてきて、author のところも変にコピペしたかなといった風に思って package.json を確認しましたが何も問題はありませんでした。そもそもこの author と collaborars は関係なさそうです。なので、authorである自分の写真が表示されないことはそもそも正常な挙動でした。

さらに調査するために collaborators のところからその前任者のページに飛ぶと、そこには共有アカウントの名前が表示されていました。なんと、そもそもプロジェクトの共有アカウントそのものに前任者の顔写真が紐づいていました。つまり、僕がやらかす前にすでに前任者自らが、共用アカウントに自分の顔写真を紐づけるというやらかしをしていました3。では、どうして前任者は共用アカウントに自分の顔写真を紐づけてしまったのでしょうか。

Gravatar

npm はGravatarというアバター作成サービスをアイコンとして使っています。そして、npm のアイコンは Gravatar からしか登録できません(Issue#12)。つまりアカウント作成時や編集時になんらかのやらかしがあったようです。

gravatar_small-262x135.jpg

共用アカウントを作ったのは前任者です。そしてそのときに 自身の Gravatar と npm のアイコンを紐けてしまったようです。前任者になんで紐づけたかを確認したところ、それは分からないとのことでした。

推測でしかないですが、Gravatar の操作は難しく、前任者があやまった設定をしただけな気がしています。設定は このような手順でやっていくのですが、レーティングの設定や、何を持って別サービスと Gravatar のアイコンを紐づけているのかが不明瞭で、意外と苦戦します。npm は、一般的なサービスと違って、自分で写真をアップしてそれを選択してアイコンとして設定するといったことができません。そのため、なんらかの操作ミスによってこういう事故はおきてしまったのだと思います。

これはまだ笑い話ですが、次は本当のやらかし話です。

個人開発した OSS を A 社のアカウントに紐づけて publish した話

僕はジョークライブラリを npm に publish することが趣味で、日頃から publish しています。先日も ビルド時に俳句を読めるプラグインを publish したり、その前にはgeo cities コンポーネントなどを publish しました。よく友人からは「電子ゴミ職人」 と呼ばれています。

僕は、このようなジョークライブラリを、とある会社のアカウントで publish してしまいました。

なぜ自分以外のアカウントで publish したか

それは様々な会社で npm publish している関係で、色々な会社のアカウントに npm login しているからです。そしてそれらの publish をプライベート端末で行っているので、会社のアカウントに login したまま自分の OSS を publish し、自分じゃないアカウントに紐づけてしまったからです。

npm は publish 前に 「このアカウントで publish しますよ y/N」といった確認もないので、いきなり本番環境に publish できてしまいます。そのため、いまどのアカウントでログインしているかを確認しないと、全然関係ない組織に紐づけて publish してしまう ということが起き得ます。そして僕は見事にやらかしました。真面目な事業をしている会社のアカウントで、ジョークライブラリ以外の説明ができない名前のライブラリを publish してしまいました。

chikyu_inseki_syoutotsu.png

たまたま、すぐに気づいたので npm unpublish して、責任者に報告と謝罪をしました。その後は、すぐさま再発防止策とその運用フローを用意しました。その再発防止策は記事の終盤で紹介します。

ぶっちゃけ間違って publish しても消せばセーフなのでは?

アウトです。確かにやらかしても npm unpublish すれば消せるので、このケースもやらかしではないかもしれません。ただ、この npm unpublish は時間制限があるので注意が必要です。なんと、公開から72 時間後は消せなくなります。(npm-unpublish)。72 時間たつと、サポートにメールで問い合わせしないといけなく、簡単な道のりではなさそうです4。また、そもそも消しても、消すまでに多少のラグはあり、その間に第三者に見られたり install されると、配信元の信用低下に繋がり兼ねません。

なぜ npm から消せないのか

どうしてこのような厳しいルールになったのか直接的な理由は知りません。ただ、よく言われているのは、left-pad 騒動です。これは、left-pad というライブラリが作者の意向で非公開となったとき、それに依存していた他のライブラリのビルドも通らなくなり、その影響が波及し利用者の多い有名サービスのビルドですらできなくなったというものです。また、有名ライブラリが消えるとその名前を横取りしてユーザーに悪意のあるコードを DL させるということもできます。5そういった事故が起きないように、npm 社は package の削除に対して慎重な姿勢になっています。

そういった運営がされる以上、もしあなたが何か npm registry 上で情報漏洩して 72 時間経つと、半永久的にあなたが情報漏洩した事実が記録されます。注意しましょう。

left-pad 騒動について

left-pad の一連の騒動については以下の記事などで解説されています。興味のある方はご覧ください。

[作り話] もし仮に A 社アカウント login 中に B 社 prj のディレクトリで publish したら何が起きるか (情報漏洩者として名が一生涯 npm に刻まれる話)

先ほどの話は、自分で作った OSS を npm で公開した話です。もし仮に、2 社かけもちで仕事をしていて、A 社の成果物を B 社に紐づけて publish したらどうなっていたでしょうか。情報漏洩です。

npm は、その気になれば、

  1. A 社アカウントでログイン
  2. B 社プロジェクトの dir 階層に移動
  3. package.json の名前を適当に書き換える
  4. npm publish
  5. A 社の名前で B 社のソースコードを全世界に公開する

ができてしまいます。

これは完全に悪意を持った行為ですが、先ほどの僕のように無邪気なnpm publishで同じようなことが起きることも十分に考えられます6npm publishは慎重に使うべきコマンドです。

惨劇はなぜおこってしまったのか

npm publish が悪い?

内心では、「何も確認もなく npm publish できてしまうの、ちょっとカジュアルすぎるのでは!?!?」と思っています。とはいえ、npm に限らずこの手のツールはこの手のやらかしをできてしまうので、一般的なプロセスとも言えます。結局は使う側が注意しなければいけません。7

軽い気持ちで publish する自分が悪い

では、なぜ僕はすぐにちゃんとした確認もせずに pubilsh してしまったのでしょうか。それはすぐデプロイしたくなる力が働いたからです。

とりあえず deploy したくなる

  • パッケージ名 は早い者勝ちなので、とりあえず v0.0.1 ですぐ publish したい
  • 本番へのデプロイが安定するまでの修正は、本番に対するトライアンドエラーになる

1 番目はともかく(マナー的にもよくない気がするし、可能ならnamespace使えばいい8)、2 番目はどうしても起きる問題なため、カジュアルな deploy はできた方が良さそうです。その上でどう対策していけば良さそうでしょうか。

二度と惨劇を起こさないためにどうしたのか

結局のところ、個人が自分の裁量だけで publish できるのがよくないのだと思います。

publish 権限を個人に割り当てるのをやめよう

まずは個人端末からの publish を制限すべきだと思います。

そして、そもそも OSS にするつもりがないものを npm で管理するのはやめましょう。Github をレジストリ代わりに使うこともできます。(npm install で github のリポジトリからインストールする)

対策に向けて ~個人開発でも CI/CD サーバーを用意しよう~

結論から言うと CI/CD から publish しようということです。そもそもの話なのですが、ローカルからレジストリに push ってやらない行為だと思っています。Node.js に限らずレジストリにライブラリを登録していくとき、普通は CI/CD サーバー からやっているはずです。9

ところが個人レベルでは CI/CD を使って module を publish している人は意外と少ないのではないでしょうか。そこで、個人レベルでも CI/CD サーバーを導入する対策方法について紹介します。ズバリ、Github Actions を使いましょう。

npm publish の仕組み

npm publish のためには、まず npm に登録して、npm login する必要があります。これまではこの無邪気なログインでやらかしをしていました。

認証と token

npm login すると、 ~/.npmrc というファイルに token が保存されます。npm publish 時はこの token を利用して認証をクリアしています。そのため試しにこのファイルを適当な文字で書き換えてみると publish ができなくなります。

この token はコピーしちゃえば持ち出せるので、これを CI/CD サーバーに配置すれば npm publish を行うことが可能となります。

module と package.json

ところで npm publish をしたら何が公開されるのでしょうか。基本的には package.json に書かれている内容が公開されます。特に name と version が重要です。この name はそのまま publish 時の名前になり、version は name ごとに同一なものが存在できません。そのため npm publish するときは適切な名前をつけて、publish ごとにバージョンを変える必要があります。またここで登場する author は collaborators には反映されません。collaborators は npm login のユーザーに紐づくことに注意しましょう。

Github Actions で対策してみる

原理的には認証トークンの配置と、package.json の記述をすれば CI/CD サーバーからでも npm publish できます。ここでは Github Actions での例を示しますが、Circle CI でも drone といった他の CI/CD ツールでも可能です。

Github Actions はその名の通り、Github が提供している デプロイワークフロー自動化サービスです。Github と連携しているため、 .github/workflow/ 配下に workflow ファイルを設置するだけで CI/CD パイプラインを構築できます。

これは個人レベルでも CI/CD パイプラインを簡単に導入できる素晴らしいものです。これがあれば個人開発中に副業先のソースコードをインターネットにぶちまける危険性を下げることができます。10

workflow で publish を定義

CI/CD パイプラインにおける各種タスクはワークフローファイルという yaml に記述して定義します。
prd のデプロイで publish すればよさそうです。とりあえず、tag が打たれたら、それに該当するコードがビルドされてデプロイされるように設定します。

name: prd

on:
  push:
    tags:
      - v*

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [12.x]

    steps:
      - uses: actions/checkout@v1
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}
      - name: install
        run: |
          yarn install
      - name: build
        run: yarn build:prd
      - name: publish
        run: |
          npm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN
          npm publish
        env:
          CI: true
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

ここでは環境変数として NPM_TOKEN をあらかじめ Github の設定画面で設定しておき、それを config として読み込むようにしています。これは実質的にはログインに相当します。
そして、npm publish を実行しています。

むやみな publish を防ぐワークフロー定義

これでタグ打ちから publish できるようになりましたが、タグ打ちのための手間はかかるので、publish は失敗したくないです。そのためなるべく確実に publish できるように諸々のチェックを master ブランチマージ前に行うようにしましょう。そのワークフローを開発用 workflow として設定を書きます。

name: dev

on:
  push:
    branches:
      - feature/**
      - fix/**

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [12.x]

    steps:
      - uses: actions/checkout@v1
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}
      - name: install
        run: |
          yarn install
      - name: licence check
        run: |
          summary=`yarn license-checker --production --summary`
          echo $summary
          ret=`echo $summary | grep -v LGPL | grep -e GPL | wc -l`
          exit $ret
      - name: build
        run: |
          yarn build:dev
        env:
          CI: true
      - name: can i publish
        run: yarn run can-npm-publish

ここでは feature/** や fix/** ブランチへの push があると、publish できるかチェックしています。そして Github 側でこのワークフローが全部通らないと merge できないようにすれば、master ブランチのソースコードは publish の成功確率が上がります。11

ここでは、ライセンスの確認と、バージョン上げ忘れの確認をしています。特にこの can-npm-publishは諸々の確認をしてくれるので便利です。

またやらかした

こちらがやらかなさないプロジェクトとして作っていたものですが、やらかしてます。コミットログを見ればわかると思いますが、やらかしまくってます。例えば、ライセンス違反をして publish をしたり、package 名が規則にあっておらず空振りしたり、namespace で区切ったが access 設定してなくて publish に失敗したり、エントリポイントの path が間違っていて何も import できないなどやらかしています。npm publish が成功したかはどうしても本番でしか確認できないので、stable なデプロイができるようになるまでは本番でやらかす傾向になります。stable にする前はマイナーバージョンなので良い気もしてますが、もしこのタイミングで npm install されると、ユーザー側で不具合がおきる可能性があります。そのため CI/CD サービスを使ったデプロイワークフロー整備も大切ですが、本番だけでこけることも想定した対策も必要です。12


  1. そもそも社内ツールを npm に公開しちゃダメだと思うんですよね・・・ 

  2. Node.js には小さなモジュールという教義があり、分けれるものは分けた方がよいという主張があります。 

  3. そのため俺はやらかしていないのかもしれない 

  4. サポートに連絡したことないので本当は簡単な道のりなのかもしれないです。詳しい人いたら教えてください。 

  5. たまに有名ライブラリに見せかけた名前で、外部と通信しているモジュールとかも公開されていたりもするので、注意しましょう。 

  6. A, B 社双方ですでに一度でも publish していれば、名前が登録されるのでそのような事故はおきないですし、事故で公開されてもすでにインターネットで公開されている情報なので情報漏洩かどうかの観点では大丈夫です(なりすまし行為になるので全然大丈夫ではないのですが)。ただ B 社側がまだ publish していなかったり、そもそも B 社のコードがライブラリではなくアプリケーションだった場合は publish されていないものなので、情報漏洩に繋がります。 

  7. おそらくクラウドへのデプロイもこの手の事故を起こせます。(サービスの名前は伏せます。) 

  8. namespaceを区切るとpublishに失敗する可能性も上がります。(詳しくは 対策の package.jsonのpublishConfigを参照) 

  9. 僕がやらかしたところは、DevOps 的な取り組みは後回しにしていたという背景があります。そのようなチームでも記事内で説明した Github Actions を使うソリューションは効果的だと思います。 

  10. もちろん、Github Actions でなくても Github 連携できる CircleCI などを使えば同様の問題は解決できます。ただ Github Actions が今一番敷居が低いと思っているので、これを題材に選びます。 

  11. この設定は Github の setting から行える。 

  12. 具体的には、npm link や tar を組み合わせた擬似 dry run をすると良いでしょう。また stable でないことが明らかであるなら、stable でないことを README かどこかに明記しておきましょう。 

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

[mongoose]mongoDBに大量のdataをSeedする[seedgoose]

seedgooseについて

最近ポケモン関連のアプリを作成していて、ポケモン達の画像のurlをmongodbに保存しています。
なので、手軽に大量のデータをseedできるプラグインないかなーって探してたらこのプラグインを見つけて
簡単に実現できたので、備忘録として書きます。

設定より規約の思想で作られているため、多少注意点などありますのでそれも説明していきます。

git
seedgoose

執筆時点では星は9個しかついてないです。
ですが、かなりお手軽にできるのでおすすめです。

導入

npm i seedgoose

package.jsonにnpm scriptを追加します。
npm run seed でseedを流す事ができるようになります。

package.json
  "scripts": {
    "seed": "seedgoose seed"
  }

ディレクトリ構成

※必ず、以下のようにmongooseで定義したmodelの名前を複数形にしたjsonファイルをdataフォルダの配下に作成する必要があります。

─ data
  ├── pokemons.json 
pokemons.json
[
  {
    "_id": "11111111111",
    "pokedex" : "1",
    "imageUrl": "https://hogehoge.1.png",
    "name": ""
  },
  {
    "_id": "22222222222222",
    "pokedex": "865",
    "imageUrl": "https://hogehoge.865.png",
    "name": ""
  }
]
models/Pokemon.js
const mongoose = require("mongoose");

const Pokemon = mongoose.model(
  "Pokemon",
  new mongoose.Schema({
    name: {
      type: String
    },
    pokedex: {
      type: String
    },
    imageUrl: {
      type: String
    }
  })
);
module.exports = { Pokemon };

つづいて設定ファイルを作成します。
※必ずファイル名は.seedgooserc.jsとして下さい。

seedgooserc.js
require("dotenv").config({ path: ".env" });

module.exports = {
  modelBaseDirectory: "./models", 
  models: "*.js",
  data: "data",
  db: process.env.MONGO_URI 
};

seed実行

上記のファイルの作成が終わったら以下のコマンドでseedを実行します。

npm run seed

以下のように表示されていれば正常にデータが生成されています。

  create 11111111111 in pokemons

seedをやり直したい場合や、削除したい場合は、npm scriptに以下のように書けばいけるようです。

package.json
"scripts": {
  "seed": "seedgoose seed",
  "reseed": "seedgoose reseed",
  "unseed": "seedgoose unseed"
}

ここまで読んでくださりありがとうございます。
すごくシンプルで使いやすいプラグインだと思いますのでぜひ使ってみてください。

それでは!

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

【mongoose】mongoDBに大量のdataをSeedする【seedgoose】

seedgooseについて

最近ポケモン関連のアプリを作成していて、ポケモン達の画像のurlをmongodbに保存しています。
なので、手軽に大量のデータをseedできるプラグインないかなーって探してたらこのプラグインを見つけて
簡単に実現できたので、備忘録として書きます。

設定より規約の思想で作られているため、多少注意点などありますのでそれも説明していきます。

git
seedgoose

執筆時点では星は9個しかついてないです。
ですが、かなりお手軽にできるのでおすすめです。

導入

npm i seedgoose

package.jsonにnpm scriptを追加します。
npm run seed でseedを流す事ができるようになります。

package.json
  "scripts": {
    "seed": "seedgoose seed"
  }

ディレクトリ構成

※必ず、以下のようにmongooseで定義したmodelの名前を複数形にしたjsonファイルをdataフォルダの配下に作成する必要があります。

─ data
  ├── pokemons.json 
pokemons.json
[
  {
    "_id": "11111111111",
    "pokedex" : "1",
    "imageUrl": "https://hogehoge.1.png",
    "name": ""
  },
  {
    "_id": "22222222222222",
    "pokedex": "865",
    "imageUrl": "https://hogehoge.865.png",
    "name": ""
  }
]
models/Pokemon.js
const mongoose = require("mongoose");

const Pokemon = mongoose.model(
  "Pokemon",
  new mongoose.Schema({
    name: {
      type: String
    },
    pokedex: {
      type: String
    },
    imageUrl: {
      type: String
    }
  })
);
module.exports = { Pokemon };

つづいて設定ファイルを作成します。
※必ずファイル名は.seedgooserc.jsとして下さい。

seedgooserc.js
require("dotenv").config({ path: ".env" });

module.exports = {
  modelBaseDirectory: "./models", 
  models: "*.js",
  data: "data",
  db: process.env.MONGO_URI 
};

seed実行

上記のファイルの作成が終わったら以下のコマンドでseedを実行します。

npm run seed

以下のように表示されていれば正常にデータが生成されています。

  create 11111111111 in pokemons

seedをやり直したい場合や、削除したい場合は、npm scriptに以下のように書けばいけるようです。

package.json
"scripts": {
  "seed": "seedgoose seed",
  "reseed": "seedgoose reseed",
  "unseed": "seedgoose unseed"
}

ここまで読んでくださりありがとうございます。
すごくシンプルで使いやすいプラグインだと思いますのでぜひ使ってみてください。

それでは!

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

【Hyperledger Fabric】Fabricのコンテナ群とSDKアプリを別々のマシンで動かす時の注意点

はじめに

Fabricのコンテナ群とSDKアプリを別々のマシンで動かす時の注意点を書きます。
今回はfabric-samplesfirst-networkをネットワーク設定に用いるものとします。
fabric-node-sdkを使ったSDKアプリはdockerコンテナとして動いています。

状況を表したものが以下です。
マシン2ではCAやordererも動作していますが、図からは除いています。
スクリーンショット 2019-12-21 23.47.47.png

設定

SDKアプリを使用する際に用いるconnection profileは以下になります。

{
    "name": "first-network-org1",
    "version": "1.0.0",
    "client": {
        "organization": "Org1",
        "connection": {
            "timeout": {
                "peer": {
                    "endorser": "300"
                }
            }
        }
    },
    "organizations": {
        "Org1": {
            "mspid": "Org1MSP",
            "peers": [
                "peer0.org1.example.com",
                "peer1.org1.example.com"
            ],
            "certificateAuthorities": [
                "ca.org1.example.com"
            ]
        }
    },
    "peers": {
        "peer0.org1.example.com": {
            "url": "grpcs://マシン2のipアドレス:7051",
            "tlsCACerts": {
                "pem": "-----BEGIN CERTIFICATE----------END CERTIFICATE-----\n"
            },
            "grpcOptions": {
                "ssl-target-name-override": "peer0.org1.example.com",
                "hostnameOverride": "peer0.org1.example.com"
            }
        },
        "peer1.org1.example.com": {
            "url": "grpcs://マシン2のipアドレス:8051",
            "tlsCACerts": {
                "pem": "-----BEGIN CERTIFICATE----------END CERTIFICATE-----\n"
            },
            "grpcOptions": {
                "ssl-target-name-override": "peer1.org1.example.com",
                "hostnameOverride": "peer1.org1.example.com"
            }
        }
    },
    "certificateAuthorities": {
        "ca.org1.example.com": {
            "url": "https://マシン2のipアドレス:7054",
            "caName": "ca-org1",
            "tlsCACerts": {
                "pem": "-----BEGIN CERTIFICATE----------END CERTIFICATE-----\n"
            },
            "httpOptions": {
                "verify": false
            }
        }
    }
}

invokeやqueryの際にfabricのネットワークに接続する処理は以下のようになります。
asLocalhostfalseにしておかなければいけません。

await gateway.connect(ccpPath, { wallet, identity: 'user1', discovery: { enabled: true, asLocalhost: false } });

そして肝がSDKコンテナのdocker-composeファイルです。
SDKアプリがコンテナではない場合も、マシン1でドメイン名の解釈を以下のように設定する必要があります。

docker-compose.yml
extra_hosts:
      - "peer0.org1.example.com:マシン2のipアドレス"
      - "peer1.org1.example.com:マシン2のipアドレス"
      - "peer0.org2.example.com:マシン2のipアドレス"
      - "peer1.org2.example.com:マシン2のipアドレス"
      - "orderer.example.com:マシン2のipアドレス"

参考記事

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

CentOSにプリインストールされたnodejsとnpmをアップデートする

nvmとかだとログインしてるユーザごとにパスが通らないといけなくて手間だったのでプリインストールされてるnodeとnpmをアップデートした

docker run -w /root --rm -it --entrypoint /bin/sh centos:centos7
# node.jsのv10をインストール
curl -sL https://rpm.nodesource.com/setup_10.x | bash -
yum remove nodejs npm
su
yum install nodejs -y
# 確認
bash-4.2# node -v
v10.18.0
bash-4.2# npm -v
6.13.4
bash-4.2#
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む