- 投稿日:2021-10-26T22:31:59+09:00
Google Photosからランダムな1枚の画像を取得できるようにする
前回の投稿 GoogleAPIライブラリを使わずにGoogleアカウントでログインできるようにする で、とりあえず、Google Photos APIにアクセスする準備として、Googleアカウントでログインできる環境を作りました。 今度は、ログインしたアカウントのGoogle Photosの画像リストを取得して、ランダムに選択した画像を取得できるようにします。 ソースコードもろもろは前回同様以下にあります。 poruruba/GooglePhotosGallery Google Photos APIを利用できるようにする Google Cloud Platformのコンソールから、Google Photos APIを使えるように有効化します。 GCP:APIとライブラリ https://console.cloud.google.com/apis/library 検索のところに、Photosと入力すると、Photos Library APIが出てきますので、選択してEnableにします。 Google Photos APIを呼び出す Google Photos APIの呼び出しには、いずれもGoogleアカウントログインで取得したアクセストークンが必要です。 今回実際に使うGoogle Photos APIは以下の通りです。 共有アルバムリストを取得 アルバムリストを取得 アルバムを作成 画像ファイルをアップロード アップロードした画像を登録 アルバムまたは共有アルバムに含まれる画像リストを取得する これらのAPIがあれば、Instagramに投稿した画像をGoogle Photosのアルバムに登録したり、アルバムに登録した画像や共有アルバムにある画像からランダムに1つを選ぶことができそうです。 それ以外に関連するものとして以下が挙げられます。 インスタグラムに投稿した画像リストを取得(※1の投稿で作成したサーバのWebAPI呼び出し) 画像ファイルを取得(通常のHTTP Get呼び出し) Googleアカウントでログインして、認可コードを取得する。(※2の投稿で作成したサーバのページに遷移) 認可コードからトークンに変換(※2の投稿で作成したサーバのWebAPI呼び出し) トークンをリフレッシュトークンで再生成(※2の投稿で作成したサーバのWebAPI呼び出し) IDトークンからユーザ名を取得 ※1 Instagramにアップロードした画像をランダムにESP32に表示する ※2 GoogleAPIライブラリを使わずにGoogleアカウントでログインできるようにする ●共有アルバムリストを取得 Input:アクセストークン Output:[アルバムID、アルバムタイトル、など] https://developers.google.com/photos/library/guides/list#listing-shared-albums ただし、OutputのnextPageTokenが含まれていた場合は、1回の呼び出しではすべてのアルバムを取得しきれなかったことを表しており、pageTokenにnextPageTokenの値を市営して再度呼び出します。 node.js/api/controllers/googlephotos-api/index.js var album_list = await do_get_with_token('https://photoslibrary.googleapis.com/v1/sharedAlbums', {}, json.access_token); var albums = album_list.sharedAlbums || []; while(album_list.nextPageToken ){ var params = { pageToken: album_list.nextPageToken }; album_list = await do_get_with_token('https://photoslibrary.googleapis.com/v1/sharedAlbums', params, json.access_token); if (album_list.sharedAlbums ) albums = albums.concat(album_list.sharedAlbums); } ●アルバムリストを取得 Input:アクセストークン Output:[アルバムID、アルバムタイトル、など] https://developers.google.com/photos/library/guides/list#listing-albums OutputのnextPageTokenによる再呼び出しは同様です。 node.js/api/controllers/googlephotos-api/index.js var album_list = await do_get_with_token('https://photoslibrary.googleapis.com/v1/albums', {}, json.access_token); var albums = album_list.albums || []; while( album_list.nextPageToken ){ var params = { pageToken: album_list.nextPageToken }; album_list = await do_get_with_token('https://photoslibrary.googleapis.com/v1/albums', params, json.access_token); if( album_list.albums ) albums = albums.concat(album_list.albums); } ●アルバムを作成 Input:アクセストークン、アルバムタイトル Output:アルバムIDなど https://developers.google.com/photos/library/guides/manage-albums#creating-new-album node.js/api/controllers/googlephotos-api/index.js var params2 = { album: { title: ALBUM_NAME } }; album = await do_post_with_token('https://photoslibrary.googleapis.com/v1/albums', params2, json.access_token); ●画像ファイルをアップロード Input:アクセストークン、画像バッファ、Mime-Type Output:アップロードトークン https://developers.google.com/photos/library/guides/upload-media#uploading-bytes node.js/api/controllers/googlephotos-api/index.js for (const instagram of instagram_list.list) { var item = media_list.find(item => item.filename.startsWith("instagram_" + instagram.id + '.')); if (item) continue; var buffer = await do_get_buffer(instagram.media_url); // console.log(buffer); var ftype = await filetype.fromBuffer(buffer); var uploadToken = await do_post_buffer('https://photoslibrary.googleapis.com/v1/uploads', buffer, ftype.mime, json.access_token); consooe.log(updateToken); params.newMediaItems.push({ simpleMediaItem: { fileName: 'instagram_' + instagram.id + '.' + ftype.ext, uploadToken: uploadToken } }); } if (params.newMediaItems.length > 0) { var result2 = await do_post_with_token('https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate', params, json.access_token); console.log(result2); } ●アップロードした画像を登録 Input:アクセストークン、[ファイル名、アップロードトークン] Output:[画像のIDなど] https://developers.google.com/photos/library/guides/upload-media#creating-media-item ●アルバムまたは共有アルバムに含まれる画像リストを取得する Input:アクセストークン、アルバムIDまたはアルバムID Output:[画像のID、画像のURLなど] https://developers.google.com/photos/library/guides/list#listing-library-contents OutputのnextPageTokenによる再呼び出しは同様です。 node.js/api/controllers/googlephotos-api/index.js var params = { albumId: albumId, pageSize: 100 } var result2 = await do_post_with_token('https://photoslibrary.googleapis.com/v1/mediaItems:search', params, access_token); var media_list = []; if (result2.mediaItems) media_list = result2.mediaItems; while (result2.nextPageToken) { params.pageToken = reulst2.nextPagetoken; result2 = await do_post_with_token('https://photoslibrary.googleapis.com/v1/mediaItems:search', params, access_token); media_list = media_list.concat(result2.mediaItems); } 上記は、ガイドのページ(https://developers.google.com/photos/library/guides/get-started)を参照しましたが、詳細はリファレンス(https://developers.google.com/photos/library/reference/rest)があります。 なんかできそうですよね。 以下に注意事項を示しておきます。 アクセストークンの有効期限は60分弱です。ですが、リフレッシュトークンがあれば、再生成できます。 リフレッシュトークンは、3か月間全く使わないと無効になります。 「アルバムまたは共有アルバムに含まれる画像リストを取得する」で取得した画像のURLの有効期限は60分です。 ●アクセストークンの有効期限は60分弱です。ですが、リフレッシュトークンがあれば、再生成できます。 有効期限が切れた場合は、リフレッシュトークンで再生成すればよいですが、有効期限ぎりぎりのタイミングで利用するのは得策ではないので、有効期限10分前だったら再生成するようにします。 node.js/api/controllers/googlephotos-api/index.js async function read_token() { var json = await jsonfile.read_json(TOKEN_FILE_PATH); if (!json || !json.albumId) { console.log('file is not ready.'); throw 'file is not ready.'; } var date = new Date(); if (date.getTime() > json.created_at + json.expires_in * 1000 - 10 * 60 * 1000) { console.log('timeover'); var params = { refresh_token: json.refresh_token }; var result = await do_post_with_apikey(api_url + '/googleapi-refreshtoken', params, API_KEY); json.access_token = result.access_token; json.expires_in = result.expires_in; json.created_at = date.getTime(); await jsonfile.write_json(TOKEN_FILE_PATH, json); await update_image_list(json); } return json; } ●リフレッシュトークンは、3か月間全く使わないと無効になります。 クロンで毎月1回、read_token()を呼び出すようにします。 node.js/api/controllers/googlephotos-api/index.js exports.trigger = async (event, context, callback) => { console.log('googlephotos.trigger cron triggered'); var json = await read_token(); console.log(json); }; node.js/api/controllers/googlephotos-api/cron.json { "enable": true, "schedule": "0 0 0 1 * *", "handler": "trigger" }, ●「アルバムまたは共有アルバムに含まれる画像リストを取得する」で取得した画像のURLの有効期限は60分です。 ちょうど、アクセストークンの有効期限と同じなので、リフレッシュトークンによるアクセストークンの更新のタイミングで、取得してあった画像リストも更新しておきます。 read_token() の関数において、トークンをリフレッシュした後に、update_image_list() を呼び出しています。 Instagramの画像をGoogle Photosのアルバムに登録する やっていることは、 Instagramにある画像のリストを取得する Google Photosのアルバムまたは共有アルバムに、Instagramの画像が登録されているかを検索する(Instagramの画像のIDを含ませたファイル名をキーにして)。登録されていない場合は、Instagramの画像のIDをファイル名として、Google Photosのアルバムに登録する。 一応、Instagramの画像がJPEGではないかもしれないので、npmモジュール「file-type」でファイルのMime-Typeを調べてから登録しています。 node.js/api/controllers/googlephotos-api/index.js async function sync_instagram(json){ var sharedalbum_list = await jsonfile.read_json(ALBUM_LIST_FILE_PATH, []); var media_list = await get_all_image_list(json.albumId, sharedalbum_list, json.access_token); var instagram_list = await do_get(api_url + '/instagram-imagelist'); var params = { albumId: json.albumId, newMediaItems: [], }; for (const instagram of instagram_list.list) { var item = media_list.find(item => item.filename.startsWith("instagram_" + instagram.id + '.')); if (item) continue; var buffer = await do_get_buffer(instagram.media_url); // console.log(buffer); var ftype = await filetype.fromBuffer(buffer); var uploadToken = await do_post_buffer('https://photoslibrary.googleapis.com/v1/uploads', buffer, ftype.mime, json.access_token); consooe.log(updateToken); params.newMediaItems.push({ simpleMediaItem: { fileName: 'instagram_' + instagram.id + '.' + ftype.ext, uploadToken: uploadToken } }); } if (params.newMediaItems.length > 0) { var result2 = await do_post_with_token('https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate', params, json.access_token); console.log(result2); } console.log('success (' + params.newMediaItems.length + ')'); return params.newMediaItems.length; } この関数を1日1回実行するようにクロン化します。 node.js/api/controllers/googlephotos-api/index.js exports.trigger2 = async (event, context, callback) => { console.log('googlephotos.trigger2 cron triggered'); var json = await read_token(); var num = await sync_instagram(json); if (num > 0) await update_image_list(json); }; node.js/api/controllers/googlephotos-api/cron.json { "enable": true, "schedule": "0 0 1 * * *", "handler": "trigger2" } 画像ファイルを取得(通常のHTTP Get呼び出し) アルバムに含まれる画像リストの画像のURLを参照して、いったんサーバ側でHTTP Getを使って画像ファイルをダウンロードした後、クライアント側から取得したLCDの画面サイズに合わせてリサイズした上でクライアントに渡します。 リサイズには、npmモジュールの「sharp」を使っています。 node.js/api/controllers/googlephotos-api/index.js case '/googlephotos-image': { const width = event.queryStringParameters.width ? Number(event.queryStringParameters.width) : 480; const height = event.queryStringParameters.height ? Number(event.queryStringParameters.height) : 320; const fit = event.queryStringParameters.fit || 'cover'; var json = await read_token(); var list = await read_image_list(json); if( list.data.length <= 0 ) throw 'image_list is empty'; var index = make_random(list.data.length - 1); var image = await do_get_buffer(list.data[index].baseUrl, {}); var image_buffer = await sharp(Buffer.from(image)) .resize({ width: width, height: height, fit: fit }) .jpeg() .toBuffer(); return new BinResponse("image/jpeg", Buffer.from(image_buffer)); } IDトークンからユーザ名を取得 IDトークンは、Googleアカウント認証したときに、アクセストークンと一緒に取得できたIDトークンに含まれます。 IDトークンは、Base64URLエンコードされており、デコードするために、npmモジュール「jwt-decode」を使いました。 node.js/api/controllers/googlephotos-api/index.js case '/googlephotos-get-username': { var json = await read_token(); const decoded = jwt_decode(json.id_token); return new Response( { name: decoded.name, sub: decoded.sub }); } クライアント側の実装 以下のボタンを用意しています。 ユーザログイン:Googleアカウントログインをします。ログインした結果のトークンは、Node.jsサーバ側に保持されます。 共有アルバム選択の変更:共有アルバムのリストを表示して、フォトフレームに含めたい共有アルバムを変更します。 Instagramと同期:Instagramに投稿してある画像をGoogle Photosアルバムに登録します。 フォトフレーム画像更新:押すたびにGoogle Photosのアルバムにある画像からランダムに選んだ画像が表示されます。 Javascriptのソースコードを示しておきます。各ボタンにvue_optionsのmethodsがそれぞれ割当たるのがわかるかと思います。 Googleアカウントログインは、こちらGoogleAPIライブラリを使わずにGoogleアカウントでログインできるようにするも参考にしてください。 node.js/public/googlephotos/js/start.js 'use strict'; //const vConsole = new VConsole(); //window.datgui = new dat.GUI(); var new_win; const SCOPE = 'https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/userinfo.profile'; const login_url = 'https://【Node.jsサーバのホスト名】/googleapi-login'; const googlephotos_base_url = '【Node.jsサーバのホスト名】'; var vue_options = { el: "#top", mixins: [mixins_bootstrap], data: { state: Math.random().toString(32).substring(2), album_list: [], sharedalbum_list: [], sharedalbum_check: [], username: null, image_data: null, }, computed: { }, methods: { update_image: async function(){ var blob = await do_get_blob(googlephotos_base_url + '/googlephotos-image'); this.image_data = URL.createObjectURL(blob); }, do_login: function () { var params = { scope: SCOPE, state: this.state }; new_win = open(login_url + '?' + new URLSearchParams(params).toString(), null, 'width=480,height=750'); }, do_token: async function(qs){ console.log(qs); if( qs.state != this.state ){ alert('state mismatch'); return; } var param = { code: qs.code, redirect_uri: qs.redirect_uri }; var result = await do_post(googlephotos_base_url + '/googlephotos-account-create', param); console.log(result); this.get_albumlist(); this.get_username(); }, get_username: async function(){ var result = await do_post(googlephotos_base_url + '/googlephotos-get-username'); console.log(result); this.username = result.name; }, get_albumlist: async function(){ var result = await do_post(googlephotos_base_url + '/googlephotos-get-albumlist' ); console.log(result); var result2 = await do_post(googlephotos_base_url + '/googlephotos-get-sharedalbum'); console.log(result2); var list = []; result.list.map(item =>{ var album = result2.list.find(item2 => item2.id == item ); if( album ) list.push(album) }); this.album_list = list; }, call_albumlist_change: async function(){ var result = await do_post(googlephotos_base_url + '/googlephotos-get-sharedalbum'); this.sharedalbum_list = result.list; this.sharedalbum_check = []; for (var i = 0; i < this.sharedalbum_list.length ; i++ ){ if (this.album_list.findIndex(item => item.id == this.sharedalbum_list[i].id ) >= 0 ) this.sharedalbum_check[i] = true; else this.sharedalbum_check[i] = false; } this.dialog_open('#albumlist_change_dialog'); }, do_albumlist_change: async function(){ var list = []; for( var i = 0 ; i < this.sharedalbum_list.length ; i++ ){ if( this.sharedalbum_check[i] ) list.push( this.sharedalbum_list[i].id ); } await do_post(googlephotos_base_url + '/googlephotos-update-albumlist', { list: list }); this.get_albumlist(); this.dialog_close('#albumlist_change_dialog'); }, sync_instagram: async function(){ try{ this.progress_open(); var result = await do_post(googlephotos_base_url + '/googlephotos-sync-instagram'); alert( String(result.num) + '個の画像を取り込みました。' ); }finally{ this.progress_close(); } } }, created: function(){ }, mounted: async function(){ proc_load(); try{ await this.get_albumlist(); await this.get_username(); }finally{ loader_loaded(); } } }; vue_add_data(vue_options, { progress_title: '' }); // for progress-dialog vue_add_global_components(components_bootstrap); vue_add_global_components(components_utils); /* add additional components */ window.vue = new Vue( vue_options ); function do_get_blob(url, qs) { const params = new URLSearchParams(qs); return fetch(params.toString() ? url + `?` + params.toString() : url, { method: 'GET', }) .then((response) => { if (!response.ok) throw 'status is not 200'; // return response.json(); // return response.text(); return response.blob(); // return response.arrayBuffer(); }); } 以上
- 投稿日:2021-10-26T22:28:17+09:00
obniz feat.BAIKIN-MAN!
パパの目論見 うちの次女はアンパンマンが大好きでバイキンマンが大嫌いな典型的な2歳児なのですが、 ガチャポンで出てくるのは、決まってバイキンマンだったりします 今回の企画はバイキンマンを使ったおもちゃを作り、 娘に好きになってもらおうバイキンマンを克服してもらおうというものです。 逃げる者を追いたくなるのは人間の性でしょうから、近づいたら逃げるバイキンマンを作ります。 名付けて、「BYE-BAIKIN-MAN!!」 obnizを手に入れたので、いざトライ! 試作品 超振動距離センサとサーボモーターを組み合わせて、動力にしたいと考えました。 この超振動を使えば、地面を叩いてやつらは動くはずである!これは期待できそうだ! 完成品「BYE-BAIKIN-MAN?」 小刻みに震えるバイキンマンが出来上がった。。。笑 ガチャポン排出品のサイズ感を考えてなかった。。。 番外編 その後タイヤが大きいと、かろうじて前進できることを確認。 ただし、バイキンマンではないので却下! こっから先は力学?な気がしたので、追い求めるのをやめました。笑 あと、もともとは、動く前にランプが点滅する無駄仕様を取り入れていたのですが、 施行繰り返し過ぎたせいか破壊してしまい、焦げ臭かったのでやめました。 材料 ●obniz Board 1Y ●マイクロサーボ 9G SG90 ●超音波距離センサモジュール HC-SR04 ソースコード const Obniz = require('obniz'); const obniz = new Obniz('****-****'); // Obniz_IDに自分のIDを入れます obniz.onconnect = async function () { // 超音波測距センサを利用する const servo = obniz.wired('ServoMotor', { gnd: 0, vcc: 1, signal: 2 }); // サーボモータを利用 const hcsr04 = obniz.wired('HC-SR04', { gnd: 11, echo: 10, trigger: 9, vcc: 8 }); // RGB LEDを利用 ➡不使用 //const rgbled = obniz.wired('WS2811', { gnd: 4, vcc: 5, din: 6 }); // ディスプレイ obniz.display.clear(); // クリア obniz.display.print('Ready'); // setIntervalで一定間隔で処理 setInterval(async function () { // 距離を取得 let distance = await hcsr04.measureWait(); // そのままだと小数点以下の桁数がやたら多いので整数に丸めてもよい //distance = Math.floor(distance); // 距離(mm)をターミナルに表示 console.log(distance + ' mm'); // obnizディスプレイに表示 // 一度消してから距離+mmの単位を表示 obniz.display.clear(); obniz.display.print(distance + ' mm'); // 距離がある程度未満かどうかの判定 if (distance < 100) { // XXmm = Xcm 以下の場合 // obnizディスプレイに近接していることを表示 obniz.display.clear(); obniz.display.print('GO!BAIKIN-MAN!'); //rgbled.rgb(255, 0, 0); // カウンターで回転回数を制御。180度と0度を繰り返す。 let cnt = 0; var angle = 0; let timerId = setInterval(async function(){ cnt++ if(cnt===5){ clearInterval(timerId); } else { servo.angle(angle); if (angle == 0) { angle = 180; } else { angle = 0; } } //rgbled.rgb(0, 0, 0); }, 1000); } }, 1000); // 1000ミリ秒 = 1秒おきに実行 } 実証実験 せっかくなのでこどもに見せてみました。 (妻のNGが出たので画像/動画はナシです。すみません。) どうやら、小刻みに震えるバイキンマンでも、見慣れないおもちゃは面白いようです。 分解後、もっかい作ってと言われたので、それなりに面白かったのでしょう。 丁重にお断りしました 次回作るときはちゃんと前進するバイキンマンを作ってあげたいものです
- 投稿日:2021-10-26T22:19:17+09:00
node.jsでHIITのTimerを作った
フィヨルドブートキャンプのプラクティスでnpmを自作しました。 せっかくなのでqiitaで公開させてもらおうと思ったので、勇気を出して書かせてもらってます。 「もっとこうした方がいいんじゃない?」などのご意見がありましたらコメントしてもらえると嬉しいです! どんなnpmを作ったのか 作ったnpmは、最近流行りのトレーニング手法であるHIIT(High Intensity Interval Training)のタイマーです。 話題の筋トレ「HIIT」とは? HIITとは、「High Intensity Interval Training(高強度インターバルトレーニング)」の略で、負荷の高い運動と小休憩を繰り返すトレーニング法のこと。限界まで体を追い込むことで、常に脂肪が燃焼しやすい状態をキープし、体脂肪減少と筋肉増量効果を得るものです。 実際に作成したnpmはこちらで確認できます↓ - npm:hiit_timer - GitHub:github.com/R-Tsukada/hiit-timer 実際どんな感じで動くのか ワークアウト時間の選択 インターバル時間の選択 1,2を何セット行うかを選択 作成したコード 最終的なコード const log = require('single-line-log').stdout const player = require('play-sound')(opts = {}) const { prompt } = require('enquirer') const emoji = require('node-emoji'); async function hiitTimer(){ const workoutTimer = await prompt( [ { type: 'select', name: 'workoutTime', message: 'select workout time', choices: ['10', '20', '30', '40', '50', '60'] }, { type: 'select', name: 'restTime', message: 'select rest time', choices: ['10', '20', '30', '40', '50', '60'] }, { type: 'select', name: 'set', message: 'select set number?', choices: ['1', '2', '3', '4', '5', '7', '8', '9', '10'] } ] ) const workout = workoutTimer.workoutTime const rest = workoutTimer.restTime const set = workoutTimer.set running(set, workout, rest) } async function running(setNumber, workout, rest){ await log('\x1b[32m10 seconds before Workout') await firstWorkout(10) for (let i = 0; i < setNumber; i++) { log(emoji.get('fist') + ` Set${i + 1} Workoout Start ` + emoji.get('fist')) await intervalTimer(workout) log((emoji.get('relaxed')) + ` Set${i + 1} Break ` + (emoji.get('relaxed'))) await intervalTimer(rest) } await log((emoji.get('congratulations')) + ' Workout Finish' + (emoji.get('exclamation')) + 'Good Job' + (emoji.get('exclamation')) + (emoji.get('congratulations'))) } function intervalTimer(selectWorkout) { return new Promise(resolve => { setTimeout(() => { const startEffect = player.play('start_effect.mp3') resolve() }, selectWorkout * 1000) } ) } function firstWorkout(second) { return new Promise(resolve => { setTimeout(() => { const startEffect = player.play('start_effect.mp3') resolve() }, second * 1000) }) } hiitTimer() 悩んだ点 今回色々悩んだことが、「ワークアウトとインターバルを設定したセット数を繰り返す」ことでした。 繰り返すということはsetIntervalを使えばいいのでは?と考えて、↓のような考え方をしていました。 console.log('First Workuout Start') const running = (function(){ console.log('First Workuout Start') let number = 1 setInterval(function() { if (number === 2) { console.log('break') } else if (number === 3) { console.log('Workuout Start') } number += 1 }, 1000) }) async function play(hiitNumber){ for (let i = 0; i < hiitNumber; i++) { running() }} play(1) このコードでも一応実装したい通りの動きになったのですが、、 First Workuout Start break Workuout Start play(3)のようにセット数を複数回繰り返すと、 First Workuout Start break break break Workuout Start Workuout Start Workuout Start break、Workuout Startが連続で処理されてしまってインターバルのタイマーになりませんでした。 非同期的に処理されてしまってるので、同期処理になるようにコードを変える必要性があるなと思いました。 修正した点 非同期処理が原因でインターバルタイマーの動きになってないので、同期処理になるようにコードを修正。 そもそもsetIntervalが非同期処理を前提にしたメソッドなので、setIntervalではなく、setTimeoutを繰り返し処理を使うことにしました。 intervalTimerというメソッドを作成して、workout(運動時間)、rest(休憩時間)の時間をそれぞれ引数で渡します。 まずはenquireを使ってworkout, rest, set(セット数)を決めます。 sync function hiitTimer(){ /* enquireで,workout、rest、setを選択 */ const workoutTimer = await prompt( [ { type: 'select', name: 'workoutTime', message: 'select workout time', choices: ['10', '20', '30', '40', '50', '60'] }, { type: 'select', name: 'restTime', message: 'select rest time', choices: ['10', '20', '30', '40', '50', '60'] }, { type: 'select', name: 'set', message: 'select set number?', choices: ['1', '2', '3', '4', '5', '7', '8', '9', '10'] } ] ) const workout = workoutTimer.workoutTime const rest = workoutTimer.restTime const set = workoutTimer.set running(set, workout, rest) } enquireで選択したworkout, restをsetTimeout に渡す。 function intervalTimer(selectWorkout) { return new Promise(resolve => { setTimeout(() => { const startEffect = player.play('start_effect.mp3') resolve() }, selectWorkout * 1000) } ) } async/await を使って同期処理で実行。 async function running(setNumber, workout, rest){ await log('\x1b[32m10 seconds before Workout') await firstWorkout(10) for (let i = 0; i < setNumber; i++) { log(emoji.get('fist') + ` Set${i + 1} Workoout Start ` + emoji.get('fist')) await intervalTimer(workout) log((emoji.get('relaxed')) + ` Set${i + 1} Break ` + (emoji.get('relaxed'))) await intervalTimer(rest) } await log((emoji.get('congratulations')) + ' Workout Finish' + (emoji.get('exclamation')) + 'Good Job' + (emoji.get('exclamation')) + (emoji.get('congratulations'))) } これでHIITのタイマー機能を実装することができました! 改善したい点 最後に改善したい点について書きます。 アドバイス等がありましたらコメントをいただけると嬉しいです! node_modules/hiit_timerのディレクトリに移動しなければ使えない 作成したnpmはmp3を使用しているので、mp3が入ってるnode_modules/hiit_timerに移動しないと使えないです。。 リファクタリングが不十分 もう少しスッキリしたコードになるんじゃないかと思うのですが、具体的にどうしたらいいのかわからず。。
- 投稿日:2021-10-26T21:28:53+09:00
LINE Botに格言を表示
前回の記事ではobnizに格言を表示しました。 今回は格言を表示してくれるLineBotを作成しました。 流れ まずはこちらの記事を参考にLine Botを作ります。 おうむ返しまで完了させます。 server.jsを変更するので一度、control + Cでnode server.jsを終了させます。 mylinebotフォルダの直下で npm i axiosを実行します。 そうするとpackage.jsonの中にaxiosが追加されます。 server.jsファイル内の'use strict'の次の行あたりにAPIを叩くための呪文const axios = require('axios');を記載。 コード終わりの方の//実際に返信の言葉を入れる箇所のところに表示させたいAPIの情報を記載。 全体像は次の通り 'use strict'; //APIを叩ける const axios = require('axios'); const express = require('express'); const line = require('@line/bot-sdk'); const PORT = process.env.PORT || 3000; const config = { channelSecret: 'チャネルシークレット', channelAccessToken: 'チャネルアクセストークン' }; const app = express(); app.get('/', (req, res) => res.send('Hello LINE BOT!(GET)')); //ブラウザ確認用(無くても問題ない) app.post('/webhook', line.middleware(config), (req, res) => { console.log(req.body.events); //ここのif分はdeveloper consoleの"接続確認"用なので削除して問題ないです。 if(req.body.events[0].replyToken === '00000000000000000000000000000000' && req.body.events[1].replyToken === 'ffffffffffffffffffffffffffffffff'){ res.send('Hello LINE BOT!(POST)'); console.log('疎通確認用'); return; } Promise .all(req.body.events.map(handleEvent)) .then((result) => res.json(result)); }); const client = new line.Client(config); async function handleEvent(event) { if (event.type !== 'message' || event.message.type !== 'text') { return Promise.resolve(null); } const res = await axios.get('https://api.adviceslip.com/advice'); //APIのURL console.log(res.data.slip.advice); return client.replyMessage(event.replyToken, { type: 'text', text: res.data.slip.advice //実際に返信の言葉を入れる箇所 }); } app.listen(PORT); console.log(`Server running at ${PORT}`); これでLINE Botになにか話かけると格言のAPIがランダムで出力されます。 こんな感じです〜。 ありがとうございました!
- 投稿日:2021-10-26T15:09:51+09:00
CHIRIMENをNode.jsで使用してみる⑵ ー GPIO編 ー
はじめに 本記事では、複数回に分けてCHIRIMENコミュニティが提供しているnpmパッケージnode-web-gpioとnode-web-i2cを使用してNode.jsからGPIOとI2Cを制御するために私が実施した方法とそのサンプルコードを記載した備忘録になっています。 今回は前回の記事に引き続きExamplesにあるGPIO ExamplesのコードをNode.jsから実行可能にする方法について説明していきます。 コードの主な違い Webブラウザ版とNode.js 版でのコードの主な違いについて説明します。 navigator()関数がNode.jsでは使用不可、記述する必要がないため削除する。 sleep()関数がNode.js には存在しないため、使用する場合は適宜実装する必要がある。 その他、HTML下では動作するがNode.js下では動作しない記法を変更する必要がある。 ex)下記GPIO-Buttonなどの"ledPort.write(val === 0 ? 1 : 0);"という表記はNode.js では使用できない。 GPIO-Blink CHIRIMENからLEDを点滅させるGPIO-Blinkについて説明します。 使用パーツ LED x 1 抵抗(100Ω〜470Ω) x 1 (220Ωのカラーコード:赤赤茶金) ジャンパー線(オス・メス) x 2 ブレッドボード x 1 配線 以下の図のように配線してください。 Webブラウザ版のJavaScript // プログラムの本体となる関数です。await で扱えるよう全体を async 関数で宣言します。 async function main() { // 非同期関数は await を付けて呼び出します。 const gpioAccess = await navigator.requestGPIOAccess(); // GPIO を操作する const port = gpioAccess.ports.get(26); // 26 番ポートを操作する await port.export("out"); // ポートを出力モードに設定 // 無限ループ while (true) { // 1秒間隔で LED が点滅します。 await port.write(1); // LED を点灯 await sleep(1000); // 1000 ms (1秒) 待機 await port.write(0); // LED を消灯 await sleep(1000); // 1000 ms (1秒) 待機 } } // await sleep(ms) と呼ぶと、指定 ms (ミリ秒) 待機 // 同じものが polyfill.js でも定義されているため省略可能 function sleep(ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); } // 宣言した関数を実行します。このプログラムのエントリーポイントです。 main(); Node.js版のJavaScript // node-web-gpioを呼び出すおまじない const { requestGPIOAccess } = require("node-web-gpio"); // sleep関数の実装 const sleep = require("util").promisify(setTimeout); async function blink() { const gpioAccess = await requestGPIOAccess(); // LEDの接続されているポートの指定 const port = gpioAccess.ports.get(26); await port.export("out"); // 点滅した回数を指定する引数 i var i = 0; while (i < 5) { await port.write(1); await sleep(1000); await port.write(0); await sleep(1000); i = i + 1; } } blink(); 実行してみよう 下記のようにコマンドラインから実行し、LEDが5回点滅すれば成功です。 % node gpio-ledblink.js GPIO-Button CHIRIMENからタクトスイッチの入力を読み込み、タクトスイッチを押すとLEDが点灯、離すと消灯します。なお、GPIOポートの読み込みにはonchange関数を使用しています。 使用パーツ タクトスイッチ x 1 LED x 1 抵抗(100Ω〜470Ω) x 1 (220Ωのカラーコード:赤赤茶金) ジャンパー線(オス・メス) x 4 ブレッドボード x 1 配線 以下の図のように配線してください。 Webブラウザ版のJavaScript main(); async function main() { var gpioAccess = await navigator.requestGPIOAccess(); var ledPort = gpioAccess.ports.get(26); // LEDの付いているポート await ledPort.export("out"); var switchPort = gpioAccess.ports.get(5); // タクトスイッチの付いているポート await switchPort.export("in"); switchPort.onchange = function(val) { // スイッチはPullupで離すと1なので反転させる ledPort.write(val === 0 ? 1 : 0); }; } Node.js版のJavaScript // タクトスイッチでLEDの点灯を行うチュートリアル const { requestGPIOAccess } = require("node-web-gpio"); async function button() { const gpioAccess = await requestGPIOAccess(); // LEDの接続されているポートの指定 const ledPort = gpioAccess.ports.get(26); await ledPort.export("out"); // タクトスイッチの接続されているポートの指定 const switchPort = gpioAccess.ports.get(5); await switchPort.export("in"); // 点灯回数をカウントする引数cnt var cnt = 0; switchPort.onchange = function(val) { // ボタンを押すと点灯、離すと消灯する if (cnt > 9 && val.value == 0){ console.log("Light : OFF"); process.exit(1); } if(val.value == 1){ ledPort.write(0); console.log("Light : OFF"); cnt = cnt + 1; } else if(val.value == 0){ ledPort.write(1); console.log("Light : ON"); cnt = cnt + 1; } }; } button(); 実行してみよう 下記のようにコマンドラインから実行し、タクトスイッチを押すとLEDが点灯すれば成功です。5回繰り返すと終了します。 % node gpio button.js Light : OFF Light : ON Light : OFF Light : ON Light : OFF GPIO-readGpioValue CHRIMENからタクトスイッチの状態(押されているか、押されていないか)をコンソールへ出力します。なお今回は、GPIO-Buttonとは異なり、GPIOポートの読み込みにread関数を使用したポーリング処理によって読み込みを実現しています。 使用パーツ タクトスイッチ x 1 ジャンパ線(オス・メス)x 2 ブレッドボード x 1 配線 以下の図のように配線してください。 Webブラウザ版のJavaScript main(); async function main() { var gpioAccess = await navigator.requestGPIOAccess(); console.log("GPIO ready!"); var port = gpioAccess.ports.get(5); await port.export("in"); while (true) { var value = await port.read(); console.log("unixtime:" + new Date().getTime() + " gpio(5)= " + value); await sleep(500); } } Node.js版のJavaScript // タクトスイッチの入力状態を確認するチュートリアル const { requestGPIOAccess } = require("node-web-gpio"); // sleep関数の実装 const sleep = require("util").promisify(setTimeout); async function button() { const gpioAccess = await requestGPIOAccess(); console.log("GPIO ready!"); const switchPort = gpioAccess.ports.get(5); await switchPort.export("in"); // タクトスイッチの入力回数をカウントする引数cnt var cnt = 0; // タクトスイッチの入力状況を把握する引数flag var flag = 0; while(cnt != 10) { const value = await switchPort.read(); if (flag == 1 && value == 1){ console.log("Button : OFF"); flag = 0; cnt = cnt + 1; } else if (flag == 0 && value == 0) { console.log("Button : ON"); flag = 1; cnt = cnt + 1; } } process.exit(1); } button(); 実行してみよう 下記のようにコマンドラインから実行し、タクトスイッチを押すとON、離すとOFFのように入力状況が出力されると成功です。 % node gpio-readgpiovalue.js GPIO ready! Button : ON Button : OFF Button : ON Button : OFF Button : ON Button : OFF GPIO-pirSensor 使用パーツ 人感センサー (KP-IR412) x 1 (※ジェネリック部品(HC-SR501)でも可) ジャンパー線(オス・メス)x 6 ブレッドボード x 1 配線 以下の図のように配線してください。 Webブラウザ版のJavaScript main(); async function main() { var gpioAccess = await navigator.requestGPIOAccess(); var sensor = document.getElementById("sensor"); var dPort = gpioAccess.ports.get(12); await dPort.export("in"); dPort.onchange = function(v) { if (v === 1) { sensor.innerHTML = "ON"; } else { sensor.innerHTML = "OFF"; } }; } Node.js版ブラウザ版のJavaScript // KP-IR412で人体赤外線感知を行うチュートリアル const { requestGPIOAccess } = require("node-web-gpio"); async function ir412() { const gpioAccess = await requestGPIOAccess(); // KP-IR412の接続されているポートの指定 const port = gpioAccess.ports.get(12); await port.export("in"); // センサーの検知回数をカウントする引数cnt var cnt = 0; // センサーで感知した結果を出力する // ただしセンサーの仕様上、ONからOFFにリセットされるまでに4〜6秒かかる port.onchange = function(v) { if (cnt > 3 && v.value == 0){ console.log("Sensor : OFF"); process.exit(1); } if (v.value == 1) { console.log("Sensor : ON"); cnt = cnt + 1; } else { console.log("Sensor : OFF"); cnt = cnt + 1; } }; } ir412(); 実行してみよう 下記のようにコマンドラインから実行し、人感センサーによる感知のON/OFFが3回行われると終了します。 % node gpio-kp-ir412.js Sensor : ON Sensor : OFF Sensor : ON Sensor : OFF Sensor : ON Sensor : OFF さいごに 今回は以上になります。今回はnode-web-gpioでのGPIOの使用方法についての記事でした。次回はnode-web-i2cで使用可能なI2Cパーツの使用方法についての記事になる予定です。
- 投稿日:2021-10-26T14:19:46+09:00
@google-cloud/connect-firestoreで保存されるセッションデータに作成時刻を追加する。
まえがき GAEでセッション管理にFirestoreを使うという内容のガイドがGoogleから公開されています。 Firestore でのセッション処理 | Node.js | Google Cloud 非常にわかりやすいのですが気になる事が一つ Connect-firestore は、古いセッションや期限切れのセッションを削除しません。Google Cloud Console でセッション データを削除するか、自動削除戦略を実装できます。セッションに、Memcache や Redis などのストレージ ソリューションを使用すると、期限切れのセッションが自動的に削除されます。 流石にコンソールから手作業で削除するのは面倒くさいので、古いセッションを自動で削除するようにしたい所。 要するにCloud MemorystoreやCloud SQLを使ってほしいということらしい。 しかしConnect-firestoreはデータをJSON文字列として保存する仕様のため、古いセッションの削除を行うにもクエリが使えない。なら保存時に作成時刻のフィールドを書き込むようにしてしまおうというのがこの記事の内容。 そもそもなんで作成時刻がないのか 実はissueが作成されたが特に何かあったわけでもなく終了している。 Session data cleanup · Issue #63 · googleapis/nodejs-firestore-session 曰く 「セッションデータの中に有効期限が入っているので別途作成時刻を含める必要はない。Firestore自体に自動削除機能は無いから必要性が薄い。」とのこと やること FirestoreStoreを継承したクラスを作り、setメソッドをオーバーライドする。 まずFirestoreStoreを継承してsetメソッドをオーバーライド Firestoreに書き込む際に作成時刻も含めるようにして、それをexportすればおk。 sessionStore.ts import { FirestoreStore, StoreOptions } from "@google-cloud/connect-firestore"; export class FirestoreStoreMod extends FirestoreStore { constructor(storeOption: StoreOptions) { super(storeOption); } set = async ( sid: string, session: unknown, callback?: ((err?: Error | undefined) => void) | undefined ): Promise<void> => { let sessJson; try { sessJson = JSON.stringify(session); } catch (err) { if (typeof callback === "function") { return callback(); } } const username: string | null = (session as any)?.passport?.user?.username || null; const createdDate = new Date(); // 本当はサーバータイムスタンプの方が良い。 await this.db .collection(this.kind) .doc(sid) .set({ data: sessJson, createdDate, username }) .then(() => { if (typeof callback === "function") { callback(); } }); }; } importして使う。 あとはガイドと同じようにsessionモジュールを読み込む際にimportして呼び出せばok。 index.ts import express from "express"; import session from "express-session"; import { Firestore } from "@google-cloud/firestore"; import { FirestoreStoreMod } from "./components/sessionStore"; const app = express(); // jsonの取り扱い用 app.use(express.json()); app.use(express.urlencoded({ extended: true })); const store = new FirestoreStoreMod({ dataset: new Firestore({ projectId: "ebikaniuni", keyFilename:"opabinia.json", }), kind: "sessions", }); // セッション app.use( session({ secret: "same", // dotenv等で別ファイル化+gitignoreに含める resave: false, saveUninitialized: false, cookie: { maxAge: 1 * 60 * 60 * 100, // ミリ秒で指定 sameSite: "strict", // ドメインが違うなら送信しない。 httpOnly: true, //httpリクエスト以外で送信されない secure: false, // 本番時は有効にする }, store,// 上で宣言したstore }) ); app.listen(3000, () => { console.log("server is started."); }); コンソールで見てこの様になっていれば成功 あとはcronなどで定期的に古いセッションを一括で削除する処理を追加すれば良いはず。 参考資料 Firestore でのセッション処理 | Node.js | Google Cloud Session data cleanup · Issue #63 · googleapis/nodejs-firestore-session @google-cloud/connect-firestore documentation
- 投稿日:2021-10-26T13:53:36+09:00
Chrome-driver のアップデート
環境 $ node --version v15.3.0 $ npm list chromedriver app@1.0.0 \app └── chromedriver@2.46.0 Google Chrome バージョン: 95.0.4638.54(Official Build) (64 ビット) Selenium npm run e2e 実行時にエラーになる $ npm run e2e > app@1.0.0 e2e > node test/e2e/runner.js Starting selenium server... started - PID: 80376 [Login] Test Suite ====================== Running: Login { parser: "babylon" } is deprecated; we now treat it as { parser: "babel" }. Error retrieving a new session from the selenium server Connection refused! Is selenium server started? { value: { message: 'session not created: Chrome version must be between 71 and 75\n' + ' (Driver info: chromedriver=2.46.628402 (536cd7adbad73a3783fdc2cab92ab2ba7ec361e1),platform=Windows NT 10.0.19042 x86_64) (WARNING: The server did not provide any stacktrace information)\n' + 'Command duration or timeout: 1.32 seconds\n' + "Build info: version: '3.141.59', revision: 'e82be7d358', time: '2018-11-14T08:25:53'\n" + "System info: host: '***', ip: '***', os.name: 'Windows 10', os.arch: 'amd64', os.version: '10.0', java.version: '15.0.1'\n" + 'Driver info: driver.version: unknown', error: 'session not created' }, status: 33 } npm ERR! code 1 npm ERR! path \app npm ERR! command failed npm ERR! command C:\WINDOWS\system32\cmd.exe /d /s /c node test/e2e/runner.js npm ERR! A complete log of this run can be found in: npm ERR! \AppData\Local\npm-cache\_logs\2021-10-25T05_25_32_549Z-debug.log ※マスキングあり エラーメッセージの内容 session not created: Chrome version must be between 71 and 75 (Driver info: chromedriver=2.46.628402 (536cd7adbad73a3783fdc2cab92ab2ba7ec361e1) googe翻訳 セッションが作成されていません:Chromeのバージョンは71から75の間である必要があります (ドライバー情報:chromedriver = 2.46.628402(536cd7adbad73a3783fdc2cab92ab2ba7ec361e1) メッセージ的には「Chrome のバージョン合わせろ」と読めますが、ダウングレードはしたくないので、chrome-driver をアップデートする chrome-driver がアップデートされない Chrome Driver NPM | npm.io を参考に、アップデートを実行する。その後、npm list chromedriver でバージョン確認したが、アップデートされない。 $ npm list chromedriver app@1.0.0 \app └── chromedriver@2.46.0 $ npm install --save-dev chromedriver --chromedriver_version=LATEST $ npm list chromedriver app@1.0.0 \app └── chromedriver@2.46.0 結論 インストール済みの chrome-driver を削除後、chrome-driver の最新版をインストールする # chrome-driver の削除 $ npm rm --save-dev chromedriver # chrome-driver のインストール $ npm install --save-dev chromedriver --chromedriver_version=LATEST # chrome-driver があるか確認 $ npm list chromedriver app@1.0.0 \app └── chromedriver@94.0.0 npm run e2e が成功する事を確認する $ npm run e2e > app@1.0.0 e2e > node test/e2e/runner.js Starting selenium server... started - PID: 78188 [Login] Test Suite ====================== Running: Login e2e { parser: "babylon" } is deprecated; we now treat it as { parser: "babel" }. √ Element <#app> was visible after 113 milliseconds. √ Element <form > .form-actions > button> was visible after 61 milliseconds. √ Element <#app > p> was visible after 583 milliseconds. √ Testing if the URL equals "http://localhost:8080/#/". OK. 4 assertions passed. (16.371s) 以上
- 投稿日:2021-10-26T02:34:07+09:00
Azure Functions と LINE Notify の組み合わせ(Node.js を利用、ポータルで開発)
この記事は、Azure Functions に HTTPリクエストを送ると、LINEアプリに通知が来る(LINE Notify を利用)という仕組みを軽く試したものです。 Azure Functions周りの開発や設定は、Azure のポータル上で行いました。 手順の概要 今回の手順は、おおまかには以下のとおりです。 LINE Notify のトークンを取得 Azure のポータル上で Azure Functions を使えるようにする Azure Functions でのコーディング前の準備 axios をインストール 環境変数を設定 Azure Functions でのコーディング 実際に進めていく LINE Notify のトークンを取得 LINEアプリへの通知を簡単化するため、LINE Notify を利用します。 ググったりすると情報が出てくると思いますので、手順の詳細は割愛しますが、おおまかには以下のような流れです。 まず、LINE Notify のページにログインをして、画面右上のメニューから「マイページ」へ移動します。 そこで、「アクセストークンの発行(開発者向け)」という部分があるので、「トークンを発行する」と書かれたボタンを押します。 その後のトークンの発行画面では、トークンの名前(何か自分が分かりやすいもの)と、LINEアプリ上で通知を送る先となるトークルームを指定します。 上記の設定を行って、「発行する」ボタンを押すと、以下のトークンが表示される画面が出てきます。 以下の画面で表示されるトークンは、再表示ができないため、ここで必ずコピーしてメモなどしておいてください。 Azure Functions 関連 サンプルを動かす まずは、以下の公式ドキュメントの手順の「関数をテストする」の部分まで進めてください。 ●Azure Portal で初めての関数を作成する | Microsoft Docs https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-create-function-app-portal サンプルは、以下のような内容になるかと思います。 module.exports = async function (context, req) { context.log('JavaScript HTTP trigger function processed a request.'); const name = (req.query.name || (req.body && req.body.name)); const responseMessage = name ? "Hello, " + name + ". This HTTP triggered function executed successfully." : "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."; context.res = { // status: 200, /* Defaults to 200 */ body: responseMessage }; } プログラムを書いた後、ブラウザなどから HTTPリクエストを送り、意図した通りのレスポンスが返ってくれば OK です。 axios を使えるようにする この後、LINE Notify を使った通知の処理で、axios を利用していきます。 Azure Functions で axios を使えるようにするために、パッケージのインストールを行います。 今回は、ポータル上でそれを進めます。 ポータルのメニューで「開発ツール」 > 「コンソール」と進み、以下のような画面を開きます。 あとは以下の記事にもあるように、上記の画像の右側の部分で npm init や npm install を行っていき、axios をインストールします。 ●Azure Functions に npm install で Node モジュールを追加する|まくろぐ https://maku.blog/p/9t7hs4e/ 環境変数を追加する 上の手順で発行した LINE Notify のトークンを、プログラムの中に直書きするのではなく、環境変数として設定して利用する形にしていきます。 そのために、ポータル上で環境変数の追加を行います。 ポータルのメニューで「設定」 > 「構成」と進み、「アプリケーション設定」の中の「新しいアプリケーション設定」を押します。 そうすると、以下の画面が出てくるので、名前と値をそれぞれ設定しましょう。 自分は、以下のようにしました。 名前: LINE_NOTIFY_TOKEN 値: 【LINE Notify のトークン】 プログラムに手を加える あとは、LINE Notify と連携する処理を加えたプログラムを作成します。 上で試したサンプルを元に、axios で POST する処理を加えたりしました。 LINE Notify のトークンは、上で環境変数に追加していたので、そこから読み込みます。 module.exports = async function (context, req) { context.log('JavaScript HTTP trigger function processed a request.'); const name = (req.query.name || (req.body && req.body.name)); const responseMessage = name ? "Hello, " + name + ". This HTTP triggered function executed successfully." : "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."; context.res = { body: responseMessage }; const axios = require('axios'); const querystring = require('querystring'); const messageLineNotify = name ?`Hello! ${name} from Azure` :`Hello! from Azure`; const res = await axios({ method: 'post', url: 'https://notify-api.line.me/api/notify', headers: { 'Authorization': 'Bearer ' + process.env["LINE_NOTIFY_TOKEN"], 'Content-Type': 'application/x-www-form-urlencoded', }, data: querystring.stringify({ message: messageLineNotify, }), }); context.log(res.data) } あとは、動作確認を行うだけです。 動作確認 以下のように、HTTPリクエストをトリガーにして、LINE Notify を使った LINEアプリへの通知ができました。 元の仕組みの中で、URL の末尾に「クエリ文字列の値 ?name=【文字列】 」があった場合、その文字列がレスポンスに使われるという部分がありました。 LINEアプリへの通知の処理でも、この文字列の有無によって、通知の文章を少し変えるようなことをやっています。 おわりに 環境変数を設定したり、axios による POST の処理を使うような形で、Azure Functions と LINE Notify の組み合わせを試すことができました。 今回、シンプルな内容で試しましたが、さらに別のアプリやサービスとの連携なども加えたりできればと思います。
- 投稿日:2021-10-26T00:23:46+09:00
Node.jsのバリデーター「Ajv」で独自formatを作る(カスタムバリデーション)
概要 ある時、typescriptで作成する入力フォームのバリデーションをしていて困ったことになった。 入力した値を、どうしても文字列の状態で上限値チェックしなければならなくなった。 具体的には、unixtimeが上限を超えないかどうかをチェックしなければならないのだが、値は文字列の状態で受け取り、また文字列の状態でレスポンスしなければならない。 使用していたバリデーション用のライブラリがAjvで、色々と調べたら独自のバリデーションができることを見つけたので、備忘録として残しておく。 標準の文字列判定 普通に文字列判定をしたいだけであれば、以下で事足りる。 import Ajv from "Ajv" const ajv = new Ajv() // 検証スキーマを定義 const schema = { type: 'object', properties: { timestamp: { type: 'string', }, }, }; const timestamp = "2147483647" //const timestamp = 2147483647 // バリデーション関数を作成 const validate = ajv.compile(schema); // バリデーションを実行 const valid = validate({timestamp}); if (!valid) { console.log(validate.errors) }else{ console.log({result: true}); } 実行結果、文字列の場合 { result: true } 実行結果、数値型の場合 [ { keyword: 'type', dataPath: '.timestamp', schemaPath: '#/properties/timestamp/type', params: { type: 'string' }, message: 'should be string' } ] カスタムバリデーション 以下のように、 ajv.addFormat で、独自の判定フォーマット「custom-timestamp」を定義する。 判定フォーマットは、properties内の「format」で指定する。 import Ajv from "Ajv" const ajv = new Ajv() ajv.addFormat('custom-timestamp', { validate: (timestampString: String) => { const timestampNumber = Number(timestampString) return timestampNumber < 2147483648 } }) // 検証スキーマを定義 const schema = { type: 'object', properties: { timestamp: { type: 'string', format: 'custom-timestamp', }, }, }; const timestamp: String = "2147483647" // バリデーション関数を作成 const validate = ajv.compile(schema); // バリデーションを実行 const valid = validate({timestamp}); if (!valid) { console.log(validate.errors) }else{ console.log({result: true}); } 実行結果、「"2147483647"(unixtime期間内)」の場合 { result: true } 実行結果、「"2147483648"(unixtime期間外)」の場合 [ { keyword: 'format', dataPath: '.timestamp', schemaPath: '#/properties/timestamp/format', params: { format: 'custom-timestamp' }, message: 'should match format "custom-timestamp"' } ] 他のバリデーションライブラリでも、似たようなことはできるかもしれない。 また、今回は数字を文字列のまま大小比較するという実装だったが、実際にはもっと複雑なロジックを記述することも可能なので、用途は広い。 以上。
- 投稿日:2021-10-26T00:11:03+09:00
nodejsのバージョン管理ツールVoltaを使ってみた
Nodejsのバージョン管理ツール googleトレンドを見ると、最近voltaが来ている感じ、、。 githubのスター数比較 - Node.js管理ツールの更新頻度とスター数を比較してみた Voltaとは Speed ⚡ Seamless, per-project version switching Cross-platform support, including Windows and all Unix shells Support for multiple package managers Stable tool installation—no reinstalling on every Node upgrade! Extensibility hooks for site-specific customization 公式引用 Rustに組み込まれてて早い。がポイントなんですかね。 使ってみる # install Volta curl https://get.volta.sh | bash # install Node volta install node # start using Node node パスを通す export VOLTA_HOME="$HOME/.volta" export PATH="$VOLTA_HOME/bin:$PATH" 公式によると volta pin node@12 このようにpinとするとバージョンが固定されるらしい。 もうちょっと使ってみて更新する予定。