20201010のNode.jsに関する記事は7件です。

Lambda&Serverless Frameworkで、multipart/form-data形式でデータを受け取る

前書き

最近業務でAWSのLambdaとServerless Frameworkを使うことが多いです。
業務の流れが早く、次から次へと新しいツールを触ってすべて忘れていきそうなので、個人的なまとめとして、記事を作成します。
言語はnode.js, TypeScriptを使用しています。

multipart/form-dataとは?

multipart/form-dataは、前述の通り複数の種類のデータを一度に扱える形式で、主な利用シーンはHTMLフォームです。特にファイルアップロードでよく利用されます。
引用元:
[フロントエンド] multipart/form-dataを理解してみよう
https://www.yoheim.net/blog.php?q=20171201

ボディはBoundaryという区切り文字で、ファイル送信時に指定したnameごとに区切られます。

------WebKitFormBoundaryO5quBRiT4G7Vm3R7
Content-Disposition: form-data; name="message"

Hello
------WebKitFormBoundaryO5quBRiT4G7Vm3R7
Content-Disposition: form-data; name="file"; filename="a.txt"
Content-Type: text/plain

aaa
------WebKitFormBoundaryO5quBRiT4G7Vm3R7--

curl
curl -X POST -F file=@./sample.csv https~(Lambda関数のエンドポイント(URL)) --output output.csv

(引用元同じ)

serverless.tsでの設定

serverless.ts
const serverlessConfiguration: Serverless = {
  service: {
    name: 'sls-test',
  },
  frameworkVersion: '1',
  custom: {
    webpack: {
      webpackConfig: './webpack.config.js',
      includeModules: true
    },
    // ここ
    apiGateway: {
      binaryMediaTypes: ['multipart/form-data']
    }
  },

これで、APIGatewayの設定→バイナリメディアタイプという欄に、multipart/form-dataが設定されるようになります。

Lambda関数内での処理

node.jsで、aws-lambda-multipart-parserというnpmモジュールを使って、multipart/form-data形式のデータを解析しています。
他の言語でも、「言語名 multipart/form-data」と調べれば、同じようなライブラリを検索できると思います。

handler.ts
// 受け取ったevent.bodyがbase64エンコードされているのでデコード
event.body = Buffer.from(event.body, 'base64').toString('binary');

// multipart/form-dataをパースする
const multipartBuffer = Multipart.parse(event, true);

// 再度base64エンコードされているのでデコード
let data: string = Buffer.from(multipartBuffer.file.content, 'base64').toString('binary');

後書き

他にも、serverless-jest-pluginというnpmモジュールを使ってテストを構築する方法や、そもそもBufferについても勉強してまとめたいのですが、一旦止めておきます。

参考

serverlessを使ってLambdaにmultipart/form-dataでバイナリデータをアップロードする
https://qiita.com/YoukyMurakami/items/a1da42b394970979ad1e

base64ってなんぞ??理解のために実装してみた
https://qiita.com/PlanetMeron/items/2905e2d0aa7fe46a36d4

Node.jsのバッファについて
http://info-i.net/buffer#i-2

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

gulpで画像を圧縮する

はじめに

「この記事を見ればgulpで画像を圧縮する手順がわかる!」という記事です。

画像の圧縮ツールは色々とあり、有名なところですとTinyPNGを使用している方も多いと思います。
しかし、本番公開前の画像などを外部サービスで圧縮するのはどうなんだい、という会社もあると思いますので、今回はgulpを使用してローカル環境で圧縮する方法をまとめました。

参考になったらぜひLGTMをお願いします!!

事前準備

gulpの環境構築の方法については、以前gulp超入門という記事を執筆していますので、そちらを参考にしてください。
環境構築はこちらの記事を元にできているものとして進めて行きます。

gulp超入門

ディレクトリ構成は以下の通りです。src配下が開発環境で、dist配下に圧縮した画像が吐き出されます。

ディレクトリ構造
プロジェクト名
 ├ dist
 │  └ image
 ├ node_modules
 ├ src 
 │  └ image
 ├ package.json
 └ gulpfile.js

使用するプラグインです。

プラグイン バージョン
Node.js 12.14.1
npm 6.13.4
npm 6.13.4
gulp 4.0.2
gulp-imagemin 7.1.0
imagemin-gifsicle 7.0.0
imagemin-mozjpeg 9.0.0
imagemin-pngquant 9.0.1
imagemin-svgo 8.0.0

プラグインをインストール

ターミナル
$ npm install --save-dev gulp-imagemin imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo

インストールが完了するとpackage.jsonにプラグインが追加されます。

スクリーンショット 2020-10-10 18.30.42.png

gulpfile.jsに記述

gulpfile.js
const gulp = require('gulp');
const imagemin = require('gulp-imagemin');
const mozjpeg = require('imagemin-mozjpeg');
const pngquant = require('imagemin-pngquant');
const imageminGif = require('imagemin-gifsicle');
const imageminSvg = require('imagemin-svgo');

var paths = {
    srcDir: './src',
    dstDir: './dist'
}

// 画像を格納しているフォルダのパス
var srcGlob = paths.srcDir + '/image';
var dstGlob = paths.dstDir + '/image';

// jpg,png,gif,svg画像の圧縮タスク
function imageMin() {
    return (
        // 参照するフォルダのパスを記述する
        gulp.src(srcGlob + '/**/*.+(jpg|jpeg|png|gif|svg)')
            .pipe(imagemin([
                // pngの圧縮
                pngquant({
                    quality: [0.6, 0.8]
                }),
                // jpgの圧縮
                mozjpeg({
                    quality: 85,
                    progressive: true
                }),
                // gifの圧縮
                imageminGif({
                    interlaced: false,
                    optimizationLevel: 3,
                    colors: 180
                }),
                // SVGの圧縮
                imageminSvg()
            ]
            ))
            // 圧縮したファイルの吐き出し先のパス
            .pipe(gulp.dest(dstGlob))
    );
}

// imageフォルダ配下に変更があれば自動でコンパイルしてくれる
function watchFile(done) {
    gulp.watch(srcGlob + '/**/*.+(jpg|jpeg|png|gif|svg)', imageMin);
    done();
}

// タスクの実行
exports.default = gulp.series(imageMin, watchFile);

タスクの実行

最後にコマンドを叩けば実行されます。

ターミナル
$ npx gulp

watchで監視状態になっているのでimageフォルダ配下に画像が追加されたら自動で圧縮してくれます。

最後に

最後までご覧いただきありがとうございました。
案件などによってディレクトリ構成などアレンジして使っていただければ幸いです。

「参考になったよ!」という方は、ぜひLGTMをお願いします!!

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

動かして理解するasync/await

JavaScriptのasync/awaitを動かしながら理解したい人向けです。
Node.jsで実行できます。

async/await
test()

// awaitを使う時はasyncが必要
async function test() {
    try {
        const code = await f(true)
        console.log(code)
        await f(false)
    } catch (err) {
        console.log(err)
    }
}

function f(ok) {
    // awaitする関数はPromiseを返す
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (ok) {
                // 正常終了
                resolve(200)
            } else {
                // 異常終了
                reject(500)
            }
        }, 3000)
    })
}

結果

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

Amazon Echo で Google Drive 上の mp3 を 再生する (その3)

今回の実装機能

今回で残りの機能すべてを実装、説明します。

機能実装

1. 最初から再生

PlayRequestHandler
    case('AMAZON.StartOverIntent'): {
        const audioPlayer = handlerInput.requestEnvelope.context.AudioPlayer;
        [playlistId, seed, , loop, , ] = audioPlayer.token.split(':');
        break;
    }

1 曲目から再生するため track を 0 にしますが、トークンの値を無視することで初期値の 0 のままにしています。
再生する曲自体が変わるため、再生中の曲を繰り返し再生するリピートフラグもクリア(トークンの値を無視)します。

2. シャッフル再生オン・オフ

シャッフルした曲順を 1024 文字と有限なトークンに埋め込むのは無理があるので、同じシャッフル結果を再現するための情報、乱数シードをトークンに埋め込み、毎回プレイリストをシャッフルしてその○曲目を再生するようにします。
問題は JavaScript 標準の Math.random() は乱数シードを指定できないという点で、乱数シードを指定できる疑似乱数生成器を実装します。

2.1. 疑似乱数生成器とシャッフル後曲番号取得

index.js
// ↓ rootFIleId 定義 (前回9行目) 辺りにこの行を追加
const UINT32_MAX_NEXT = 2 ** 32;

// ↓ getJson の下 (前回205行目) 辺りに以下のコードを追加

// 0~1未満の実数の疑似乱数生成器
const getSeed = () => Math.floor(Math.random() * (UINT32_MAX_NEXT - 1)) + 1;
const getNext = (() => {
    let s = Uint32Array.of(getSeed());
    return seed => {
        if (seed) s[0] = seed;
        s[0] ^= s[0] << 13;
        s[0] ^= s[0] >> 17;
        s[0] ^= s[0] << 5;
        return s[0] / UINT32_MAX_NEXT;
    };
})();

// シャッフル後の index 番目のトラック番号を取得する
// 0~length-1 の連番をランダムに並べ替え、index 番目の数を返却する
const getShuffledTrack = (length, index, seed) => {
    getNext(seed);
    let seq = [...Array(length).keys()];
    while (length > index) {
        const pick = Math.floor(getNext() * length--);
        [seq[length], seq[pick]] = [seq[pick], seq[length]];
    }
    return seq[index];
};

疑似乱数生成器は Xorshiftなどの擬似乱数をプロットして比較してみた の Xorshift 版のものを使いました。そして プレイリストの曲数分の連番を Fisher–Yates アルゴリズムでシャッフルし、その指定番目の値(元のプレイリストにおける曲番号)を返却します。
なお計算時間をケチるため、シャッフル後の指定番目の値が確定した時点で値を返すようにしています。(while (length > index) の部分)

2.2. 再生リクエスト処理

PlayRequestHandler
    case('AMAZON.ShuffleOnIntent'):
    case('AMAZON.ShuffleOffIntent'): {
        const audioPlayer = handlerInput.requestEnvelope.context.AudioPlayer;
        [playlistId, , , loop, , ] = audioPlayer.token.split(':');
        seed = requestTypeOrIntentName === 'AMAZON.ShuffleOnIntent' ? getSeed() : 0;
        handlerInput.responseBuilder.speak(`${seed ? 'シャッフル' : '最初から順番に'}再生します。`);
        break;
    }

シャッフル再生オンの場合は乱数シードを新たに生成 getSeed() し、オフの場合は 0 にします。上書きする乱数シード、オン・オフにより 1 曲目からの再生となるトラック番号、再生曲が変わり解除するリピートフラグについてはトークン値を無視します。

PlayRequestHandler
    // addAudioPlayerPlayDirective を利用して AudioPlayer に音楽再生の指示を応答する
    const idx = seed ? getShuffledTrack(playlist.length, track, seed) : track;
    const url = makeDriveUrl(playlist[idx].id);

乱数シードから実際にプレイリストの何曲目を再生するかを求めます。前回 const idx = track; となっていた部分ですが、シャッフル再生時(seed > 0)はシャッフルした track 番目の曲番号、通常再生時は track 自体の番号を使って url を求めます。

3. 前の曲、次の曲

PlayRequestHandler
    case('AMAZON.NextIntent'):
    case('AMAZON.PreviousIntent'): {
        const audioPlayer = handlerInput.requestEnvelope.context.AudioPlayer;
        [playlistId, seed, track, loop, , ] = audioPlayer.token.split(':');
        track = (+track) + (requestTypeOrIntentName === 'AMAZON.NextIntent' ? 1 : -1);
        break;
    }

発話によって曲を変更するため、すぐ指定した曲を再生するよう behaviorREPLACE_ALL のままにする点を除いて、前回説明した AudioPlayer.PlaybackNearlyFinished とほぼ同じです。
トークンから取得したトラック番号に対し、次の曲なら +1、前の曲なら -1 します。
再生する曲が変わるためリピートフラグはクリアします。

PlayRequestHandler
    if (track >= playlist.length) {
        return handlerInput.responseBuilder
            .addAudioPlayerClearQueueDirective('CLEAR_ALL')
            .withShouldEndSession(true)
            .getResponse();
    } else if (track < 0) {
        track = 0;
    }

前の曲や次の曲がプレイリストの範囲を超える場合はそれぞれ以下のようにします。

  • 前の曲:1 曲目を再生します(track = 0
  • 次の曲:再生を終了します(通常の全曲再生終了時と同じ)

このあとのループ再生でこの部分は手が入ります。

4. ループ再生オン・オフ

PlayRequestHandler
    case('AMAZON.LoopOnIntent'):
    case('AMAZON.LoopOffIntent'): {
        const audioPlayer = handlerInput.requestEnvelope.context.AudioPlayer;
        [playlistId, seed, track, , repeat, ] = audioPlayer.token.split(':');
        loop = requestTypeOrIntentName === 'AMAZON.LoopOnIntent' ? 'loop' : '';
        offset = audioPlayer.offsetInMilliseconds;
        const speakOutput = `ループ再生を${loop === 'loop' ? 'オン' : 'オフ'}にします。`;
        handlerInput.responseBuilder.speak(speakOutput);
        break;
    }

ループ再生オンかオフかでループフラグを loop'' にセットします。
ループフラグだけを変更したトークンをキューに登録しますが、そのままだと発話によって割り込んだ再生中の曲がまた最初からの再生になってしまうため、割り込み時の曲の位置 offset を取得しておき、その位置からの再生を指示することで再開したように見せかけます。

PlayRequestHandler
    if (track >= playlist.length) {
        if (loop !== 'loop') {
            return handlerInput.responseBuilder
                .addAudioPlayerClearQueueDirective('CLEAR_ALL')
                .withShouldEndSession(true)
                .getResponse();
        }
        track = 0;
    } else if (track < 0) {
        track = loop === 'loop' ? playlist.length - 1 : 0;
    }

ループフラグがオンの場合、前の曲や次の曲がプレイリストの範囲を超えても循環するようにトラック番号を設定します。

5. リピート再生

PlayRequestHandler
    case('AMAZON.RepeatIntent'): {
        const audioPlayer = handlerInput.requestEnvelope.context.AudioPlayer;
        [playlistId, seed, track, loop, , ] = audioPlayer.token.split(':');
        repeat = 'repeat';
        offset = audioPlayer.offsetInMilliseconds;
        const speakOutput = '現在の曲を繰り返し再生します。';
        handlerInput.responseBuilder.speak(speakOutput);
        break;
    }

リピートフラグに repeat をセットします。ループオン・オフと同様、割り込んだ曲の再生位置オフセットを取得しておいて再開したように見せかけます。

PlayRequestHandler
    case('AudioPlayer.PlaybackNearlyFinished'): {
        // 曲の終了間際の場合は再生中の曲をそのままにするため REPLACE_ENQUEUED でキューを置き換える
        behavior = 'REPLACE_ENQUEUED';
        const audioPlayer = handlerInput.requestEnvelope.context.AudioPlayer;
        [playlistId, seed, track, loop, repeat, ] = audioPlayer.token.split(':');
        track = (+track) + (repeat === 'repeat' ? 0 : 1);
        break;
    }

リピートフラグは「再生中の曲を繰り返し再生する」機能のため、曲の再生終了間際に次の曲をセットする処理で同じ曲番号をセットします。リピートフラグはクリアしないため同じ曲を無限再生します。リピート解除のインテントはありませんので、代わりに「次の曲」をリクエストすることで次の曲に移りつつリピートが解除できます。

6. 一時停止、再開

PlayRequestHandler
    case('AMAZON.PauseIntent'): {
        return handlerInput.responseBuilder
            .addAudioPlayerStopDirective()
            .withShouldEndSession(true)
            .getResponse();
    }
    case('AMAZON.ResumeIntent'): {
        const audioPlayer = handlerInput.requestEnvelope.context.AudioPlayer;
        [playlistId, seed, track, loop, repeat, ] = audioPlayer.token.split(':');
        offset = audioPlayer.offsetInMilliseconds;
        break;
    }

一時停止は AudioPlayer に StopDirective を送ります。再開時はトークン値とオフセットを取得してそのまま再生指示します。
ただし「Alexa、終了して」と発話した場合に AMAZON.StopIntent ではなく AMAZON.PauseIntent として扱われるみたいで、スキルを終了したつもりが一時停止になったりしますので、機能的に使わないなら実装しないか、明示的に「Alexa、プレイミュージックを終了して」と発話するか、「Alexa、キャンセルして」と CancelIntent などの別のインテントで発話するか、などの対応が必要になります。

7. ヘルプ

PlayRequestHandler
    case('AMAZON.HelpIntent'): {
        const listNames = [
            'お気に入り'
            ].join('');
        const speakOutput = `利用可能なプレイリストは、${listNames}、です。`;
        return handlerInput.responseBuilder.speak(speakOutput).getResponse();
    }

ひとりで使う分には関係ないですが、設定したプレイリスト名に何があったかを確認できるよう、ヘルプに対してプレイリスト名を列挙して返答するようにします。なんなら root.json にこれようのテキストを記述して読み込むように実装する手もあります。

8. 曲情報確認

PlayRequestHandler
    case('AskInfoIntent'): {
        const [, , , , , info] = handlerInput.requestEnvelope.context.AudioPlayer.token.split(':');
        const speakOutput = `この曲は、${info}、です。`;
        return handlerInput.responseBuilder.speak(speakOutput).getResponse();
    }

再生中の曲情報を確認するための処理で、プレイリスト json の info プロパティに記述したテキストを読み上げます。
このインテントは標準インテントではなくカスタムインテントのため、単に「Alexa、曲名を教えて」と発話しても PlayMusic スキルに対するリクエストとして処理してくれません。少し面倒ですが「Alexa、プレイミュージックで曲名を教えて」などと スキル名接続詞発話サンプル の形で発話します。
スキル名を言わなくて済む方法を探しましたが無理でした。Name-free Interactions という仕組みに可能性を感じましたが日本では対応していないようで、スキル名を言う形で妥協しました。

8.1. AskInfoIntent

PlayMusicIntent を作成した時と同様に AskInfoIntent を作成します。
image.png

曲名を訪ねる際のサンプル発話を適当に設定してください。一括編集機能を使うと1行1サンプルで記述したテキストを貼り付けられるので楽です。ここで設定したのは以下の13パターンです。

曲名
この曲
この曲名
曲名を教えて
この曲を教えて
この曲名を教えて
曲名をおしえて
この曲をおしえて
この曲名をおしえて
この曲なに
この曲はなに
この曲何
この曲は何

9. その他修正箇所

PlayRequestHandler
    canHandle(handlerInput) {
        return [
            'PlayMusicIntent', 'AMAZON.StartOverIntent',
            'AudioPlayer.PlaybackNearlyFinished', 'AMAZON.NextIntent', 'AMAZON.PreviousIntent',
            'AMAZON.ShuffleOnIntent', 'AMAZON.ShuffleOffIntent',
            'AMAZON.LoopOnIntent', 'AMAZON.LoopOffIntent', 'AMAZON.RepeatIntent',
            'AMAZON.PauseIntent', 'AMAZON.ResumeIntent',
            'AskInfoIntent', 'AMAZON.HelpIntent'
        ].includes(getRequestTypeOrIntentName(handlerInput));
    },
CancelAndStopIntentHandler
    canHandle(handlerInput) {
        return ['AMAZON.CancelIntent', 'AMAZON.StopIntent', 'SessionEndedRequest']
            .includes(getRequestTypeOrIntentName(handlerInput));
    },

対応したリクエストやインテントを CancelAndStopIntentHandler から PlayRequestHandler に移すだけです。

おわりに

これで Google Drive 版音楽プレイヤーの実装は完了です。
再生指示できるプレイリスト名を Playlist スロットタイプに登録する、Google Drive 上のファイル ID を調べてプレイリスト json ファイルを作る、root.json にそのプレイリストを記述する、など手間は掛かりますが機能的には使えるのではないかと思います。
プレイリストの作成補助として、Google Drive のフォルダからファイルを検索し、タグ情報のタイトルを読み取ってファイル ID とタイトルを列挙する Google スプレッドシート(Google Apps Script)も作ったので番外編として記事にするかも知れません。

参考

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

LINE Messaging APIを使ってブルワリーを探してみた

目的

 LINEBotを使って、対話でうまくブルワリーの情報を探したい!
 なぜLINEか?
 ⇒老若男女問わず使えるインターフェースですから!
 なぜブルワリーか?
 ⇒お酒が、、好きなんです。

前提

 記事を読まれる前に、、、
 まだまだやりたいことの最初の慣れの部分なので、
 えっ?なんで?と思うことは多いかと思います。

今回やったこと

 ブルワリー検索APIの「Open Brewery DB」と「LINE MessagingAPI」を使って、
 LINEのトーク画面からニューヨークのブルワリーを検索して、一覧をLINEのレスポンスとして返す。
 URL付きでページにとんだり、電話をかけやすくするなど。

やってみてわかったこと

 ★流れが何となく見えた。
 ユーザがLINEから何かメッセージをうつ。
 →LINEサーバにまず送られる。
 →LINEのサーバからWebhookでイベントがとんでくる。
 →イベントの何を取るのか、チェックしつつ、来た内容に対してどう返すかをプログラムで作る。
 
 リクエストのイベントが来るところの受け方は、
 詳しくはLINEのドキュメントをもっと見たほうが良いと思った。
 https://developers.line.biz/ja/reference/messaging-api/

ソース

brewery_server.js
 'use strict'; // おまじない

// ########################################
//               初期設定など
// ########################################

// パッケージを使用します
const express = require('express');
const line = require('@line/bot-sdk');
const axios = require('axios');
const fs = require('fs');

// ローカル(自分のPC)でサーバーを公開するときのポート番号です
const PORT = process.env.PORT || 3000;

// Messaging APIで利用するクレデンシャル(秘匿情報)です。
const credential = JSON.parse(fs.readFileSync('./credential/credential.json', 'utf8'));

const config = {
    channelSecret: credential.channelSecret,
    channelAccessToken: credential.channelAccessToken
};


// ユーザのメッセージに対してどう返すかを定義
const sampleFunction = async (event) => {
    // ユーザーメッセージが想定しているものかどうか
    if (check_params(event.message.text)) {
        return client.replyMessage(event.replyToken, {
            type: 'text',
            text: 'ニューヨークのブルワリーは?と話しかけてね'
        });
    } else {
        // 「リプライ」を使って先に返事しておきます
        await client.replyMessage(event.replyToken, {
            type: 'text',
            text: '調べています……'
        });

        let pushText = '';
        try {
            // axiosでニューヨークブルワリーリストのAPIを叩きます(少し時間がかかる・ブロッキングする)
            const res = await axios.get('https://api.openbrewerydb.org/breweries?by_state=new_york');
            console.log(res.data);
            const brewery_data = res.data;
            for (let j =0 ; j<brewery_data.length ; j++) {
                const b_name = brewery_data[j].name;
                const b_url = brewery_data[j].website_url;
                console.log(b_name);
                if(j === 0){
                    pushText = 'ここ!\r\n';

                }else{
                    pushText = pushText + '店名 ' + b_name + '\r\n' + 'URL ' + b_url + '\r\n\r\n';
                }
            }
        } catch (error) {
            pushText = '検索中にエラーが発生しました。ごめんね。';
            // APIからエラーが返ってきたらターミナルに表示する
            console.error(error);
        }

        // 「プッシュ」で後からユーザーに通知します
        return client.pushMessage(event.source.userId, {
            type: 'text',
            text: pushText,
        });
    }
};


function check_params(event_text) {
    if(event_text === 'ニューヨークのブルワリーは?'){
        return false;
    }else {
        return true;
    }

}

// ########################################
//  LINEサーバーからのWebhookデータを処理する部分
// ########################################

// LINE SDKを初期化します
const client = new line.Client(config);

// LINEサーバーからWebhookがあると「サーバー部分」から以下の "handleEvent" という関数が呼び出されます
async function handleEvent(event) {
    // 受信したWebhookが「テキストメッセージ以外」であればnullを返すことで無視します
    if (event.type !== 'message' || event.message.type !== 'text') {
        return Promise.resolve(null);
    }
    // サンプル関数を実行します
    return sampleFunction(event);
}



// ########################################
//          Expressによるサーバー部分
// ########################################

// expressを初期化します
const app = express();

// HTTP POSTによって '/webhook' のパスにアクセスがあったら、POSTされた内容に応じて様々な処理をします
app.post('/webhook', line.middleware(config), (req, res) => {
    // Webhookの中身を確認用にターミナルに表示します
    console.log(req.body.events);

    // 検証ボタンをクリックしたときに飛んできたWebhookを受信したときのみ以下のif文内を実行
    if (req.body.events[0].replyToken === '00000000000000000000000000000000' && req.body.events[1].replyToken === 'ffffffffffffffffffffffffffffffff') {
        res.send('Hello LINE BOT! (HTTP POST)'); // LINEサーバーに返答します
        console.log('検証イベントを受信しました!'); // ターミナルに表示します
        return; // これより下は実行されません
    }

    // あらかじめ宣言しておいた "handleEvent" 関数にWebhookの中身を渡して処理してもらい、
    // 関数から戻ってきたデータをそのままLINEサーバーに「レスポンス」として返します
    Promise.all(req.body.events.map(handleEvent)).then((result) => res.json(result));
});

// 最初に決めたポート番号でサーバーをPC内だけに公開します
// (環境によってはローカルネットワーク内にも公開されます)
app.listen(PORT);
console.log(`ポート${PORT}番でExpressサーバーを実行中です…`);

環境

 Node.js v14.9.0
 Visual Studio Code v1.49.3
 @line/bot-sdk

結果

 brewery.gif
 

【おまけ】本当は、やれたらやりたいこと

■技術
 ・ユーザから送られたメッセージのチェック(関数切り出し)
 ・LINEに表示された際に、URLから取得されたサイトのサムネイル?がMAX5件ぐらいなので、
  取得も5件ぐらいの方が見やすそう。
 ・認証情報は、見えそうなファイルよりも、もっと隠れたところから取得したほうが良いと思う。
 ・リッチメニューを使ってそもそもどんな操作をすればよいのかユーザにわかりやすくする。
  (フリーテキストって何入れればええのかわからない。)
 ・Googleマップのリンクを付けて保存しやすくする。

■企画
 ・ビールも好きだけど、肉やカレーも。。。

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

nodemonの使い方

プログラミング勉強日記

2020年10月10日
nodemonというツールを初めて見かけたので、まとめておく。

nodemonとは

 コードの変更を監視して自動でサーバーを再起動してくれるツール。
 Node.jsで動いてるアプリのコードを修正した後は、サーバーの再起動が必要になるが、コードを修正した後に毎回手動で再起動するのは面倒だし、再起動をし忘れてしまうことがある。そういった作業を自動化できるnodemonは便利である。

nodemonのインストール

 使用したいプロジェクトの下で以下のコマンドを実行する。

npm install nodemon --save-dev

 開発環境のみで使用するので--save-devを付ける。コマンドを実行するとpackage.jsonに以下のように追記される。

package.json
"devDependencies": {
    "nodemon": "^2.0.3"
  }

 本番環境においては以下のコマンドを実行すると除外される。

npm install --production

アプリの実行

 npm startで起動できるようにpackage.jsonにscritを設定する。

package.json
"scripts": {
    "start": "nodemon src/app.js"
  },

 package.jsonを変更したら以下のようにコマンドを実行してアプリを起動させる。

$ npm start

> express-api@1.0.0 start /sample/express-api
> nodemon src/app.js

 これでnodemon経由でsrc/app.jsが起動された。

参考文献

nodemonを使用して、コード変更後に自動でNode.jsのアプリを再起動する方法。
nodemonで自動再起動【開発時の手間削減】

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

node.js(express)でパスワードのハッシュ化 〜bcryptモジュールの使い方〜

bcryptとは

password$2b$10$7f9myKwdo9BDUOkybKpQoOSMeEX90aRRFfOdj.c3dG6RIjZxZ/a4mみたいな感じにぐっちゃぐちゃにしてくれるnpmモジュール

使い方

まずはrequireで読み込み

const bcrypt = require('bcrypt')

ハッシュ化

const hashed = bcrypt.hash(req.body.password, 10)
//bcrypt.hash(ハッシュ化したいpassword, ストレッチング回数)
//ストレッチング回数 =ハッシュ化の回数で、2のn乗のnのことで4~31を指定できる

照合

const compared = bcrypt.compare(req.body.password, docs.password)
//bcrypt.compare(ハッシュ化前のpassword, ハッシュ化後のpassword)

ポイント

  • bcrypt.hashは時間がかかる為、非同期処理としてコントロールする必要。async awaitをつける
  • hash後にhash化したパスワードをデータベース等に保存する処理を書くことが多いと思いますが、その場合async awaitをつけないとハッシュ化が終わる前に次の保存処理を実行してしまう為、passwordがみつかりませんよ!みたいなエラーが出る。
  • bcryptにはhashSyncというメソッドも用意されており、これを使ってもasync await同様の同期的な動きをとる。が、どこかで非推奨?と見た事があります。

使用例 

現在勉強のため制作しているTodoアプリでexpress, mongooseを使っていますので、そこで使ったユーザー登録とログイン処理での使用例を記載致します。

<新規ユーザー登録>
クライアントからのフォーム送信で受けた新規ユーザーのpasswordを、ハッシュ化しデータベースに登録

router.post('/register', async (req, res) => {
  const hashedPassword = await bcrypt.hash(req.body.password, 10)

  console.log(hashedPassword);
//hashedPasswordにpromiseが返ってくるためawaitで処理の完了を待つ
//試しにawaitを外すとPromise{pending}がlogされる
//pendingは「わたし待ってます」状態

  const userData = new User({
    username: req.body.username,
    password: hashedPassword
//ここでhashedPasswordのpromiseが解決している必要がある
//そのためbcrypt.hashにawaitをつけて10回hashされるのを待ってから次の処理に進むように制御する
  })

// --- 以下DB登録処理 --- 
    User.find({ username: userData.username }, (err, docs) => {
    if (docs.length) {
      res.send({
        msg: 'そのユーザー名は使えません'
      })
      return
    } else {
      User.insertMany(userData)
      .then(() => res.send({
        msg: '登録が完了しました!ログインしてください'
      }))
    }
  })
})

<ログインユーザーの照合>
クライアントからのフォーム送信で受けたユーザーネームとpasswordをデータベースと照合し、合致するデータを取り出す

router.post('/login', (req, res) => {
// ユーザー名でデータベース検索する処理のサンプル

 User.find({
    username: req.body.username,
  }, async (err, docs) => {

    if (!docs.length) {
      res.send({
        msg: 'ユーザー名 か パスワード が間違っています',
        isLogin: false
      })
      return
    }

// ここからハッシュ前のpasswordとDB内のハッシュ済のpasswordとの照合

    const compared = await bcrypt.compare(req.body.password, docs[0].password)
// bcrypt.compare(ユーザーが入力したpassword, DBから引っ張ってきたハッシュ化済のpassword)
// bcrypt.compareもPromiseが返してくるのでasync,awaitで制御を加える
// 照合が終わったら次の処理へ進むようになる

    console.log(compared)
// passwordがあってればtrue まちがっていればfalse


    if (!hassed) {
      res.send({
        msg: 'ユーザー名 か パスワード が間違っています',
        isLogin: false
      })
      return
    } else {
      res.send({
        msg: 'ログインできました',
        isLogin: true,
        username: req.body.username
      })
    }
  })
})

以上です。

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