- 投稿日:2020-05-18T23:32:12+09:00
Node.jsでエラー: No valid exports main found for '/SOME_PATH/node_modules/uuid'
- 投稿日:2020-05-18T20:00:06+09:00
Qiitaの自分の投稿にLGTMが付いたら通知してもらう
自分が投稿した記事に、どなたかがLGTM(いいね)してくれると、やっぱりうれしくて、書いたかいがあったなあと、元気をもらえます。
そこで、30分ごとに、LGTM(いいね)数とフォロワー数をウォッチして、増えていたら、LINEに通知と、自宅にあるGoogleHomeスマートスピーカにしゃべってもらおうと思います。
コロナの影響でずっと在宅勤務なので、ちょっとしたアクセントにもなります。ちなみに、GoogleHomeスマートスピーカは、これから立ち上げるcron実行PCと同じネットワークにある必要があります。
通知の準備
LINE通知には、LINE Notifyを使いました。
そのためには、パーソナルアクセストークンが必要です。以下にアクセスして、アクセストークンの発行(開発者向け)をします。
https://notify-bot.line.me/my/グループに発行してもよいですし、1:1でLINE Notifyから通知を受け取る でもよいです。
そうすると、43文字程度のパーソナルアクセストークンが発行されます。あとで、使います。Google Homeスマートスピーカからしゃべってもらうためには、スマートスピーカのIPアドレスが必要です。
スマートスピーカと連携済みのAndroidのGoogle Homeアプリから、デバイス設定を選ぶと、一番下の情報 というところに、IPアドレスがありますのでメモっておきます。現在のLGTM(いいね)数とフォロワー数の取得
Qiita APIを見てみたのですが、記事ごとのLGTM(いいね)数は取得できますが、合計数をとれるようなAPIは見当たりませんでした。
そこで、QiitaのWebページからスクレーピングします。スクレーピングには、npmのcheerioを使いました。
また、Webページの取得には、node-fetchを使いました。npm install cheerio
npm install node-fetchclass=” UserCounterList__UserCounterItem-sc-******” のような感じのHTMLエレメントがあるので、その近辺を探しています。
ソースはこんな感じ。
index.jsconst cheerio = require('cheerio'); const fetch = require('node-fetch'); const { URL, URLSearchParams } = require('url'); async function get_qiita_state(userid){ return do_get('https://qiita.com/' + userid, {}) .then((text) =>{ const $ = cheerio.load(text); var state = {}; var root = $("[class^='UserCounterList__UserCounterItem-sc-']"); root.each((i, elem) =>{ var label = $(elem).children("[class^='UserCounterList__UserCounterItemLabel-sc-']").text(); var num = parseInt($(elem).children("[class^='UserCounterList__UserCounterItemCount-sc-']").text()); state[label] = num; }); // Posts, Contributions, Followers console.log(state); return state; }); } function do_get(url, qs) { var params = new URLSearchParams(qs); var url2 = new URL(url); url2.search = params; return fetch(url2.toString(), { method: 'GET', }) .then((response) => { if (!response.ok) throw 'status is not 200'; return response.text(); }); }関数get_qiita_state()の呼び出しにより、
{ Posts : 投稿数, Contributions : LGTM数, Followers : フォロワー数 }が返ってきます。
LINE通知する
関数line_notifyに、通知したいメッセージと、さきほどのパーソナルアクセストークンを指定します。
index.jsconst fetch = require('node-fetch'); const { URL, URLSearchParams } = require('url'); const Headers = fetch.Headers; function line_notify(message, token){ var params = { message: message }; return do_post_urlencoded_token('https://notify-api.line.me/api/notify', params, token); } function do_post_urlencoded_token(url, params, token) { const headers = new Headers({ 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization' : 'Bearer ' + token }); var body = new URLSearchParams(params); return fetch(new URL(url).toString(), { method: 'POST', body: body, headers: headers }) .then((response) => { if (!response.ok) throw 'status is not 200'; return response.json(); }) }GoogleHomeスマートスピーカにしゃべらせる
関数homeSpeechに、メッセージと、スマートスピーカのIPアドレスを指定すると、数秒後にスマートスピーカがしゃべります。
index.jsconst Client = require('castv2-client').Client; const MediaReceiver = require('castv2-client').DefaultMediaReceiver; const googletts = require('google-tts-api'); function homeSpeech(text, host) { return googletts(text, 'ja-JP', 1) .then(function(url) { return playUrl(url, host); }); } function playUrl(url, host) { return new Promise((resolve, reject) => { var client = new Client(); client.connect(host, () => { client.launch(MediaReceiver, (err, player) => { if( err ){ console.log('Error: %s', err.message); client.close(); return reject(err); } var media = { contentId: url, contentType: 'audio/mp3', streamType: 'BUFFERED' }; player.load(media, { autoplay: true }, (err, status) =>{ client.close(); resolve('Device notified'); }); }); }); client.on('error', (err) =>{ console.log('Error: %s', err.message); client.close(); reject(err); }); }) }本体
以上の関数を呼び出すメイン部です。
いくつか、ちょっと特殊処理を入れています。LGTM数とフォロワー数は、以前のチェック時からの増加をみて、LINEとスマートスピーカから通知をしています。以前のチェック時の数は、ファイルに保存しています。
ファイル名として、QIITA_STATE_FILE を指定し、読み出しと更新用の関数をstate_read、state_updateを用意しました。また、夜中に通知されても困るので、現在時刻を見て、NOTIFY_RANGE_HOURS の配列に示した時間の時だけ通知するようにしました。
index.jsconst fs = require('fs'); const QIITA_USERID = process.env.QIITA_USERID || '【チェックしたいQiitaユーザ名】'; const GOOGLE_DEVICE_ADDRESS = process.env.DEVICE_ADDRESS || '【GoogleHomeスマートスピーカwのIPアドレス】'; const QIITA_STATE_FILE = process.env.QIITA_STATE_FILE || './data/qiita_state.json'; const LINE_PERSONAL_ACCESS_TOKEN = process.env.LINE_PERSONAL_ACCESS_TOKEN || '【LINE Notifyのパーソナルアクセストークン】'; const NOTIFY_RANGE_HOURS = process.env.NOTIFY_RANGE || [ 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 0, 1 ] get_qiita_state(QIITA_USERID) .then(async (state) =>{ var prev_state = state_read(); var date = new Date(); var hour = date.getHours(); if( NOTIFY_RANGE_HOURS.includes(hour) ){ state_update(state); var message = ""; if( state.Contributions > prev_state.Contributions ) message += 'いいねが' + (state.Contributions - prev_state.Contributions ) + '件増えたよ。'; if( state.Followers > prev_state.Followers ){ if( message != "" ) message += "\n"; message += 'フォロワーが' + (state.Followers - prev_state.Followers ) + '人増えたよ。'; } if( message != "" ){ await homeSpeech(message, GOOGLE_DEVICE_ADDRESS); await line_notify("\n" + message, LINE_PERSONAL_ACCESS_TOKEN); }else{ console.log('no change'); } } }) .catch(error =>{ console.log(error); }); function state_read(){ try{ return JSON.parse(fs.readFileSync(QIITA_STATE_FILE, 'utf8')); }catch(error){ return {}; } } function state_update(state){ fs.writeFileSync(QIITA_STATE_FILE, JSON.stringify(state), 'utf8'); }ということで、以下を環境に合わせて変更してください。
・QIITA_USERID:チェックしたいQiitaユーザ名
・GOOGLE_DEVICE_ADDRESS:GoogleHomeスマートスピーカwのIPアドレス
・QIITA_STATE_FILE:現在のLGTM数の保存先ファイル名
・LINE_PERSONAL_ACCESS_TOKEN:LINE Notifyのパーソナルアクセストークン
・NOTIFY_RANGE_HOURS:通知されてもよい時間(時)の配列cron化
シェルスクリプトを作成します。
以下例です。vi index.sh
chmod +x index.sh#!/bin/sh cd /home/XXXXX/projects/node/cron_lgtm /home/XXXXX/.nvm/versions/node/v8.12.0/bin/node index.jsあとはcronに登録するだけです。以下は30分ごとにチェックする場合です。
crontab -e
0,30 * * * * /home/XXXXX/projects/node/cron_lgtm/index.sh
以上
- 投稿日:2020-05-18T19:22:11+09:00
node-fetchをwebpackすると実行できない・・・
やりたかったこと
シンプルにとあるWebサイトをスクレイピングして、情報を取得したかった。
過去にもスクレイパーを作成したことはあるのだが、その時リクエストを送信するのにrequestモジュールを使用していたが、どうやらパッケージが2020年2月に廃止されたらしい…
そこで見つけたのが「node-fetch」。
試しにこれを使って指定したページのHTML要素を取得したい。使ってみる
TypeScriptをインストール
yarn add typescript詳しい環境構築はこの記事の本筋からズレるため省略させていただきます。
node-fetchをインストール
yarn add node-fetchここでは詳しい使い方などは省略させていただきます。
詳しくはこちらをサンプルコードを作成
index.tsimport fetch from 'node-fetch'; const url: string = "https://qiita.com/"; fetch(url) .then(res => res.text()) .then(body => console.log(body));これでHTMLが取得できるはず。
補足
JavaScriptで動的に生成されている要素を取得したい場合、ヘッドレスブラウザが必要になるようです。
この記事の本筋と異なるため、こちらも省略させていただきます。
(今回スクレイピングするサイトには必要なかったので詳しく調べてないです…ごめんなさい)実行してみる
ts-nodeを使用して実行してみる。
package.json"scripts": { "start": "ts-node ./index.ts", "build": "webpack" }実行
$ npm run start <!DOCTYPE html><html><head><meta charset="utf-8" /> <title>Qiita</title><meta content="Qiitaは、プログラマのための技術情報共有サービスです。 プログラミングに関するTips、ノウハウ、メモを簡単に記録 &amp; 公開することができます。" name="description" /> <meta content="width=device-width,initial-scale=1,shrink-to-fit=no" name="viewport" /> <meta content="#55c500" name="theme-color" /> <meta content="XWpkTG32-_C4joZoJ_UsmDUi-zaH-hcrjF6ZC_FoFbk" name="google-site-verification" /> <link href="/manifest.json" rel="manifest" /> <link href="/opensearch.xml" rel="search" title="Qiita" type="application/opensearchdescription+xml" /> <meta name="csrf-param" content="authenticity_token" /> ....省略うん。取れた。
webpack
たったこれだけなのでJavaScriptで書いても良かったのですが、今回作るプロダクトではTypeScriptを使いたかった。
というわけでwebpackでトランスパイルします。まずはwebpackのツールをインストール
yarn add -D webpack webpack-cli ts-loaderwebpack.config.jsを作成します。
webpack.config.jsconst path = require('path'); module.exports = { mode: 'development', entry: './index.ts', output: { path: path.join(__dirname, "dist"), filename: "index.js" }, module: { rules: [{ test: /\.ts$/, use: [ {loader: 'ts-loader'} ] }] }, resolve: { modules: [ "node_modules", ], extensions: [ '.ts', '.js', 'json' ] } };webpackして実行
$ node ./dist/index.js webpack:///./node_modules/node-fetch/browser.js?:11 throw new Error('unable to locate global object'); ^ Error: unable to locate global object at getGlobal (webpack:///./node_modules/node-fetch/browser.js?:11:8) at eval (webpack:///./node_modules/node-fetch/browser.js?:14:14) at Object../node_modules/node-fetch/browser.js (/Users/taisei/Documents/project/MagicReview/scraping/dist/index.js:97:1) at __webpack_require__ (/Users/taisei/Documents/project/MagicReview/scraping/dist/index.js:20:30) at eval (webpack:///./src/index.ts?:26:36) at Object../src/index.ts (/Users/taisei/Documents/project/MagicReview/scraping/dist/index.js:109:1) at __webpack_require__ (/Users/taisei/Documents/project/MagicReview/scraping/dist/index.js:20:30) at /Users/taisei/Documents/project/MagicReview/scraping/dist/index.js:84:18 at Object.<anonymous> (/Users/taisei/Documents/project/MagicReview/scraping/dist/index.js:87:10) at Module._compile (internal/modules/cjs/loader.js:1158:30)あれ?トランスパイルしたらエラーでた…
解決方法
調査した結果、以下の方法で解決しました。
webpack.config.js// 省略 target: "node", //この行を追加 module: { rules: [{ test: /\.ts$/, use: [ {loader: 'ts-loader'} ] }] }, ....どうやら、webpackでバンドルするときにtargetをnodeに指定しないとbrowserオブジェクトが取得できないようです。
省略しますが、webpack.config.jsを上記のように修正後、webpack→実行すると意図した結果が得られました。targetをnodeに指定することで、Node.js環境で実行できるようにコンパイルしてくれるようです。
参考→webpackドキュメント追記
ちなみに、axiosでやってみても同じことが起こりました。
フロントでaxiosを使用していたときには、targetを指定しなくても意図した挙動をしてくれていたのですが、サーバーサイド(Node.js環境)で使用するには必要なようです。
いい勉強になりました。
- 投稿日:2020-05-18T16:20:51+09:00
Cypress:セッションを保持した状態でテストする
はじめに
新天地で、社内システムの自動テスト化について取り組ませていただくことになりました。
テストは、独自ランナーが使いやすい、Cypressです。
英語がまったく読めないので、苦戦・・・
Chromeの翻訳機能に助けられながら、やったことをメモしておきます。
OSはMacです。自動テストの概念
自動テストについて調べた結果、概念はこんな感じです。
- できるだけ簡単なコードで作成する
- 機能別・テスト名・コード単位で検索しやすい
- 簡単に更新できる
- ひとつのものをみんなで共有出来る
- 誰かに依存しない
セッションを保持したまま、複数のテストを実施する
まずは、ログインIDとパスワードを入れ、セッションを保持したまま別のテストを行う。
という入り口的な部分。
試行錯誤をしましたが、以下の2点を活用しました。1.Cookieをファイル経由で保存する
参照サイト:Cypressで送る快適E2Eライフ
上記サイト様を参考にして、Cookie情報を別のファイルに出力し、それを読み込むという手法。
ログイン処理をカスタムコマンドにし、ログイン〜Cookieの出力まで行います。カスタムコマンドについてはこちら ▶ 公式ドキュメント
cypress/support/commands.jsCypress.Commands.add('login, () => { cy.url().then(url => { if(url === 'http://...'){ console.log('relogin') cy.clearCookie('session_id') const login_id = Cypress.env('login_id') const password = Cypress.env('password') cy.get('[name=login_id]').type(login_id).should('have.value', login_id) cy.get('[name=password]'). type(password,{ log:false }).should(el$ => { if(el$.val() !== password){ throw new Error('Different value of typed password') } }) cy.get('.submit').click() //セッションを保持 cy.getCookie('session_id').should('exist').then((cookie) => { cy.writeFile("cache/cookie/session_id.json", { value: cookie.value }) }) } }) });
cypress/support/commands.js
内容としては、もし、アクセスしたURLがログインフォームと同じだったらば、clearCookieして再ログインしてね。
セッションがあればスルーされる部分です。また、ログインIDとパスワードは、
cypress.json
ファイルに、環境変数としてセット。
真ん中の色々書いてある部分は、ログイン情報はセキュリティ上表示させないようにしてね、という処理。
ですので、ランナーのコマンドログにパスワードは表示されません。
cypress/integration/login_spec.jsdescribe('Login Action',() => { /** * セッション設定 */ before('Session Setting', () => { cy.readFile("cache/cookie/session_id.json").then(cs => { cy.setCookie("session_id", cs.value) }); Cypress.Cookies.preserveOnce('session_id') cy.visit('URL') }); /** * ログインチェック */ beforeEach ('Login Check',() => { cy.login() }) /** * テスト処理 */ it ('Test Case1', () => { // something assertion }) })
cypress/integration/login_spec.js
内容としては、セッションがあればテストを開始、
セッションが無ければ、再ログイン(カスタムコマンド)をしてからテスト開始。
before
に、ファイルに出力されたCookie情報をreadさせて、beforeEachで再ログインの有無をチェック。
因みにbeforeEachはすべてのテスト(it)に当たるので、毎回再ログインチェックしている。
別に要らないんですけどね。。。
余計な処理も、Cypress速いからまあ良いでしょう。ということ。
2.Cypress.Cookies.preserveOnce('session_id') を使う
cypress/integration/login_spec.js
ファイルに出力されたCookie情報を、保持しておくことが出来るコマンドです。
跨いで保持することは出来ないようです。Cypress.Cookies.preserveOnce('session_id')おわりに
他にも、セッションについては永続的に保持しておけるlocalStorageの利用もひとつの手段かと思いましたが、なかなか実装が出来ず。
Cypressは便利な分、不安定な要素や、実装できない要素等まだまだあるようです。
npmモジュールを入れると、ランナーが起動しなくなるし、正しく実行できるテストも、5回に1回はなんとかエラーが出ます。(ホットリロードすると正常)まだまだ勉強することはたくさんありますが、セッションの保持が一個クリア出来たので、記録します。
シンプルな方法ですが、初心者なのでこれからこれから。
他に良い方法や、おすすめの方法があれば教えていただきたいです。
- 投稿日:2020-05-18T01:06:56+09:00
aws-serverless-expressのサンプル動かないんだけど
動かないやつ
https://github.com/awslabs/aws-serverless-express/tree/master/examples/basic-starter
npm run setup
出てたエラー
An error occurred (ValidationError) when calling the CreateChangeSet operation: Stack:arn:aws:cloudformation:ap-northeast-1:363880502757:stack/AwsServerlessExpressStack/aaaaaaaaaaaaaaaaaaa is in ROLLBACK_COMPLETE state and can not be updated. npm ERR! code ELIFECYCLE npm ERR! errno 255 npm ERR! aws-serverless-express-example@2.1.1 deploy: `aws cloudformation deploy --template-file packaged-sam.yaml --stack-name $npm_package_config_cloudFormationStackName --capabilities CAPABILITY_IAM --region $npm_package_config_region` npm ERR! Exit status 255 npm ERR! npm ERR! Failed at the aws-serverless-express-example@2.1.1 deploy script. npm ERR! This is probably not a problem with npm. There is likely additional logging output above. npm ERR! A complete log of this run can be found in: npm ERR! /Users/aaaaa/.npm/_logs/2020-05-17T15_40_32_799Z-debug.log npm ERR! code ELIFECYCLE npm ERR! errno 255 npm ERR! aws-serverless-express-example@2.1.1 package-deploy: `npm run package && npm run deploy` npm ERR! Exit status 255 npm ERR! npm ERR! Failed at the aws-serverless-express-example@2.1.1 package-deploy script. npm ERR! This is probably not a problem with npm. There is likely additional logging output above. npm ERR! A complete log of this run can be found in: npm ERR! /Users/aaaaaaaa/.npm/_logs/2020-05-17T15_40_32_816Z-debug.log npm ERR! code ELIFECYCLE npm ERR! errno 255 npm ERR! aws-serverless-express-example@2.1.1 setup: `npm install && (aws s3api get-bucket-location --bucket $npm_package_config_s3BucketName --region $npm_package_config_region || npm run create-bucket) && npm run package-deploy` npm ERR! Exit status 255 npm ERR! npm ERR! Failed at the aws-serverless-express-example@2.1.1 setup script. npm ERR! This is probably not a problem with npm. There is likely additional logging output above. npm ERR! A complete log of this run can be found in: npm ERR! /Users/aaaaaaaa/.npm/_logs/2020-05-17T15_40_32_832Z-debug.log色々すっ飛ばして結論
- runtime のnodeバージョンが古い(サンプルの8系がサポート終わってる(直せや
どうするか
- サンプルプロジェクト内の
cloudformation.ymal
の 60行目付近のRuntime
を書き換える
Runtime: nodejs8.10
->Runtime: nodejs12.x
x
がミソ。バージョン指定すると動かないは?動かないんだけど
- RollBack出来ないとかなんとかなので一回Stackを消してから
npm run setup
してみよう。おわり