Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all 8820 articles
Browse latest View live

How to build a Twitter bot with NodeJs

$
0
0

Building a Twitter bot using their API is one of the fundamental applications of the Twitter API. To build a Twitter bot with Nodejs, you’ll need to take these steps below before proceeding:

Create a new account for the bot.
Apply for API access at developer.twitter.com
Ensure you have NodeJS and NPM installed on your machine.
We’ll be building a Twitter bot with Nodejs to track a specific hashtag then like and retweet every post containing that hashtag.

Getting up and running
Firstly you’ll need to initialize your node app by running npm init and filling the required parameters. Next, we install Twit, an NPM package that makes it easy to interact with the Twitter API.

$ npm install twit --save

Now, go to your Twitter developer dashboard to create a new app so you can obtain the consumer key, consumer secret, access token key and access token secret. After that, you need to set up these keys as environment variables to use in the app.

Building the bot

Now in the app’s entry file, initialize Twit with the secret keys from your Twitter app.

// index.js
const Twit = require('twit');
const T = new Twit({
consumer_key: process.env.APPLICATION_CONSUMER_KEY_HERE,
consumer_secret: process.env.APPLICATION_CONSUMER_SECRET_HERE,
access_token: process.env.ACCESS_TOKEN_HERE,
access_token_secret: process.env.ACCESS_TOKEN_SECRET_HERE
});

Listening for events

Twitter’s streaming API gives access to two streams, the user stream and the public stream, we’ll be using the public stream which is a stream of all public tweets, you can read more on them in the documentation.

We’re going to be tracking a keyword from the stream of public tweets, so the bot is going to track tweets that contain “#JavaScript” (not case sensitive).

Tracking keywords
// index.js
const Twit = require('twit');
const T = new Twit({
consumer_key: process.env.APPLICATION_CONSUMER_KEY_HERE,
consumer_secret: process.env.APPLICATION_CONSUMER_SECRET_HERE,
access_token: process.env.ACCESS_TOKEN_HERE,
access_token_secret: process.env.ACCESS_TOKEN_SECRET_HERE
});

// start stream and track tweets
const stream = T.stream('statuses/filter', {track: '#JavaScript'});
// event handler
stream.on('tweet', tweet => {
// perform some action here
});

Responding to events

Now that we’ve been able to track keywords, we can now perform some magic with tweets that contain such keywords in our event handler function.

The Twitter API allows interacting with the platform as you would normally, you can create new tweets, like, retweet, reply, follow, delete and more. We’re going to be using only two functionalities which is the like and retweet.

// index.js
const Twit = require('twit');
const T = new Twit({
consumer_key: APPLICATION_CONSUMER_KEY_HERE,
consumer_secret: APPLICATION_CONSUMER_SECRET_HERE,
access_token: ACCESS_TOKEN_HERE,
access_token_secret: ACCESS_TOKEN_SECRET_HERE
});

// start stream and track tweets
const stream = T.stream('statuses/filter', {track: '#JavaScript'});
// use this to log errors from requests
function responseCallback (err, data, response) {
console.log(err);
}
// event handler
stream.on('tweet', tweet => {
// retweet
T.post('statuses/retweet/:id', {id: tweet.id_str}, responseCallback);
// like
T.post('favorites/create', {id: tweet.id_str}, responseCallback);
})

Retweet

To retweet, we simply post to the statuses/retweet/:id also passing in an object which contains the id of the tweet, the third argument is a callback function that gets called after a response is sent, though optional, it is still a good idea to get notified when an error comes in.

Like

To like a tweet, we send a post request to the favourites/create endpoint, also passing in the object with the id and an optional callback function.

Deployment
Now the bot is ready to be deployed, I use Heroku to deploy node apps so I’ll give a brief walkthrough below.

Firstly, you need to download the Heroku CLI tool, here’s the documentation. The tool requires git in order to deploy, there are other ways but deployment from git seems easier, here’s the documentation.

There’s a feature in Heroku where your app goes to sleep after some time of inactivity, this may be seen as a bug to some persons, see the fix here.

You can read more on the Twitter documentation to build larger apps, It has every information you need to know about.

Here is the source code in case you might be interested.

Source - CodeSource.io


Custom Vision Service を使用してインテリアの樹種分析

$
0
0

CustomVisionService を 簡単なサイトを作って使えるようにしたい

今週はMicrosoftAzureから画像認識を試したのでそちらの成果を簡単に使えるサイトを作ろうと
思いました。内容はせっかくなので本業にちなんでインテリアの画像から樹種を解析してくれるものを目指しました。
内装を考える時にテーマとなる樹種を一つ決めて組み合わせていくと空間にまとまりが出るので、
自分が良いなと思う家具や内装がどの樹種なのかを調べられると買う家具や似合う色が決まってきて
インテリアを構築する時の参考になると良いなと思いました。

環境

Node.js v10.16.3
Windows 10 pro
Visual Studio Code v1.39.1

概要

①MicrosoftAzureでアカウントを作成
②Microsoft Custom Vision Service を使用して画像解析のプロジェクトを作成、トレーニング
③作成したプロジェクトをAPIとして使用する
④APIを取得する為のコードを書く

参考資料

主に①~②の参考にいつもどおり先陣の知恵をお借りします。
【資料1】Microsoft Custom Vision Service を使用した鼓膜画像認識

③、④はこの資料を参考に。
【資料2】Node.jsでAzure Face APIを使ってみる

随時わからない所があるので補足でネットサーフィンしたログを
【資料3】[axios] axios の導入と簡単な使い方
【資料3】JavaScript テキストボックスの値を取得/設定する

①MicrosoftAzureでアカウントを作成

【資料1】を参考に、、、

②Microsoft Custom Vision Service を使用して画像解析のプロジェクトを作成

【Microsoft Custom Vision】の演習に沿って新たなプロジェクトを作ります。

image.png

プロジェクトを開いたら画像を追加から画像をアップロードします。
image.png

今回は樹種を分析したいので、色味の違う樹種を三種類、
ウォルナット、チーク、メイプルの画像を用意しました。
また、分析結果が何に対してなのかわかりやすいように、用意した画像の項目を家具と部屋にわけました。

image.png

≪家具タグ≫
image.png

≪部屋タグ≫
image.png

分析結果

トレーニングで検証してみると、
image.png

99.8%家具。ちゃんと見極めてます。
材質は72.1%チーク。
7.2%のメイプルは、、、床材ですかね。
写真の要素を読み取ってくれました。

③作成したプロジェクトをAPIとして使用する

image.png
性能タブから公開し、予測URLを発行する。
image.png

④APIを取得する為のコードを書く

フォルダ構成は前回と同じ。
image.png

フォルダを作成し、中に >node_modules:node.jsのデータが入っているフォルダ
            >public:htmlデータを入れるフォルダ
             >index.html:サイトを構成する静的ファイル
            >index.js:作成したpublicの静的ファイルをexpressで表示させるコード
            >gitignore:herokuで実装する時に不要なデータを送らないよう指定するファイル
            >package-lock.json:npm init -yで作成される
            >package.json:npm init -yで作成される
             インストールしたライブラリデータ等のパッケージが登録されている
            >Procfile:Herokuを起動するのに必要なファイル

上記のファイルを作成し、Herokuまたは
http://localhost:8080/
で起動させ、確認しながら進める。

今回のコードを起動必要なライブラリは

npminit-ynpmibody-parserexpressnpmiaxios

をそれぞれターミナルに入力し、インストール。

index.html

<!DOCTYPEhtml><html><head><title>Step01</title>
<scriptsrc="https://unpkg.com/vue"></script>
</head>
<body><p>画像URL<spanid="span1"></span></p><formname="form1"> <inputtype="text"name="text1"value="red"size="100" > <!--<input>タグ内でtype="url" を指定するとURL入力欄の作成name="text1" フォーム部品の名前  value="red"送信される値を指定size="100"表示される文字数maxlength="" 入力できる最大文字数の指定--></form>
<inputtype="button"value="解析"onclick="clickBtn1()"><inputtype="button"value="クリア"onclick="clickBtn2()"><script>'use strict';// JavaScript内でuse strict を宣言すると、コードがstrict(厳格)モードで実行されるようになる。 //strictモードでは、より的確なエラーチェックが行われ、 //これまでエラーにならなかったような曖昧な実装がエラー扱いになるfunctionclickBtn1(){ // clickBtn1をクリックされた時の値を取得constt1=document.form1.text1.value; //form1のtext1のvalueに入力値をt1に代入 document.getElementById("span1").textContent=t1;}functionclickBtn2(){document.getElementById("span1").textContent="";}constaxios=require('axios');// axiosを読み込む。require使う場合constsubscriptionKey='';//キーを指定consturiBase='https://url';constimageUrl=ti;// Request parameters.constparams={'returnFaceId':'true','returnFaceLandmarks':'false','returnFaceAttributes':'age,gender,headPose,smile,facialHair,glasses,'+'emotion,hair,makeup,occlusion,accessories,blur,exposure,noise'};constconfig={baseURL:uriBase,method:'post',headers:{'Content-Type':' application/json','Prediction-Key':subscriptionKey},data:'{"url": '+'"'+imageUrl+'"}',params:params,}axios.request(config).then(res=>{constjsonResponse=JSON.stringify(res.data,null,'');console.log('JSON Response\n');console.log(jsonResponse);}).catch(error=>console.log(error.response.data));</script>
</body>
</html>

image.png

簡単なボタンが出来ました。
試しに画像のURL入力し解析をクリック。

image.png

ちゃんと の"span1"に反映されている為t1にはきちんと代入されているはず。

image.png

コードがうまく動かない。

ターミナルにヘロクのログを見るコードを打ち込んでみる。

herokulogs-t
エラー
2019-12-03T13:11:14.837911+00:00heroku[router]:at=infomethod=GETpath="/"host=s191127-sample.herokuapp.comrequest_id=1d9cfa53-4255-4901-8d40-72a1df83f945fwd="114.69.33.94"dyno=web.1connect=1msservice=4msstatus=304bytes=237protocol=https2019-12-03T13:11:54.832593+00:00heroku[router]:at=infomethod=GETpath="/"host=s191127-sample.herokuapp.comrequest_id=886df22d-3db7-4f66-8b38-92045a51b489fwd="114.69.33.94"dyno=web.1connect=1msservice=2msstatus=304bytes=237protocol=https2019-12-03T13:12:00.185770+00:00heroku[router]:at=infomethod=GETpath="/"host=s191127-sample.herokuapp.comrequest_id=5fe36a9e-5f40-4bfb-8bdf-6084058293c6fwd="114.69.33.94"dyno=web.1connect=1msservice=3msstatus=304bytes=237protocol=https2019-12-03T13:12:33.770914+00:00heroku[router]:at=infomethod=GETpath="/"host=s191127-sample.herokuapp.comrequest_id=474ad7cc-d4b9-4d21-9b81-05d9e0d0cf81fwd="114.69.33.94"dyno=web.1connect=1msservice=2msstatus=304bytes=237protocol=https2019-12-03T13:17:58.000000+00:00app[api]:Buildstartedbyusersayu5713@gmail.com2019-12-03T13:18:10.733484+00:00heroku[web.1]:Restarting2019-12-03T13:18:10.737606+00:00heroku[web.1]:Statechangedfromuptostarting2019-12-03T13:18:10.486482+00:00app[api]:Releasev4createdbyusersayu5713@gmail.com2019-12-03T13:18:10.486482+00:00app[api]:Deploye73341abbyusersayu5713@gmail.com2019-12-03T13:18:10.000000+00:00app[api]:Buildsucceeded2019-12-03T13:18:11.420251+00:00heroku[web.1]:StoppingallprocesseswithSIGTERM2019-12-03T13:18:11.483511+00:00heroku[web.1]:Processexitedwithstatus1432019-12-03T13:18:12.514200+00:00heroku[web.1]:Startingprocesswithcommand`node index.js`2019-12-03T13:18:14.818512+00:00app[web.1]:serverstart!(heroku)2019-12-03T13:18:15.462076+00:00heroku[web.1]:Statechangedfromstartingtoup2019-12-03T13:18:17.019143+00:00heroku[router]:at=infomethod=GETpath="/"host=s191127-sample.herokuapp.comrequest_id=94efca27-f6a1-48a6-8ca7-ee2305b16ed5fwd="114.69.33.94"dyno=web.1connect=1msservice=22msstatus=200bytes=1087protocol=https2019-12-03T13:52:15.499404+00:00heroku[web.1]:Idling2019-12-03T13:52:15.508200+00:00heroku[web.1]:Statechangedfromuptodown2019-12-03T13:52:19.993362+00:00heroku[web.1]:StoppingallprocesseswithSIGTERM2019-12-03T13:52:20.117700+00:00heroku[web.1]:Processexitedwithstatus1432019-12-03T13:57:16.000000+00:00app[api]:Buildstartedbyusersayu5713@gmail.com2019-12-03T13:57:28.515154+00:00heroku[web.1]:Statechangedfromdowntostarting2019-12-03T13:57:28.121717+00:00app[api]:Deployba7a5e71byusersayu5713@gmail.com2019-12-03T13:57:28.121717+00:00app[api]:Releasev5createdbyusersayu5713@gmail.com2019-12-03T13:57:28.000000+00:00app[api]:Buildsucceeded2019-12-03T13:57:30.578721+00:00heroku[web.1]:Startingprocesswithcommand`node index.js`2019-12-03T13:57:32.601844+00:00app[web.1]:serverstart!(heroku)2019-12-03T13:57:34.225781+00:00heroku[web.1]:Statechangedfromstartingtoup2019-12-03T13:57:37.046079+00:00heroku[router]:at=infomethod=GETpath="/"host=s191127-sample.herokuapp.comrequest_id=4111dd64-a841-43a0-a582-e4e4bd3ac757fwd="114.69.33.94"dyno=web.1connect=1msservice=27msstatus=200bytes=2243protocol=https2019-12-03T14:32:17.716968+00:00heroku[web.1]:Idling2019-12-03T14:32:17.721462+00:00heroku[web.1]:Statechangedfromuptodown2019-12-03T14:32:18.890822+00:00heroku[web.1]:StoppingallprocesseswithSIGTERM2019-12-03T14:32:18.992477+00:00heroku[web.1]:Processexitedwithstatus143^Cバッチジョブを終了しますか(Y/N)?Y

(´;ω;`)ウゥゥ
今回はここで断念です、、、
どうにも力不足…また出直します。。。
ありがとうございました!!

React アプリケーションのボイラープレート CLI を作って使っている話

$
0
0

この記事は ミクシィグループ Advent Calendar 2019の5日目の記事です。

React で CLI というと create-react-appが有名です。
格好良いベースを作ってくれるのですが個人的には依存 package が多いので、自分用の CLI を作ってそちらを使っています。

@yami-beta/create-ts-app

TypeScript を使ったアプリケーションのベースを作る対話型のインターフェースを持った CLI ツールです。
https://www.npmjs.com/package/@yami-beta/create-ts-app
create-ts-app.gif
意外と色々な package を用意する必要がある ESLint + Prettier の設定を含めていたり、author や LICENSE を設定できます。
(あくまで個人用なので自分の好みによせたボイラープレートになっています)

現在は React のシンプルなボイラープレートしかありませんが

  • React, React Router, Redux 等が含まれた Single Page Application
  • express によるサーバアプリケーション

のボイラープレートを追加していく予定です。

仕組み

この CLI ですが SAOというライブラリを使って実装しています。
create-nuxt-appも SAO を利用していたりします)

以下のようなコードを書くことで対話型のインターフェースを用意したり、テンプレートからファイルをコピーやリネームといったことが出来ます。

module.exports={prompts(){return[{name:'name',message:'What is the name of the new project',default:this.outFolder,filter:val=>val.toLowerCase()}]},actions:[{type:'add',files:'**'},{type:"move",patterns:{"LICENSE_*":"LICENSE"}}],asynccompleted(){this.gitInit()awaitthis.npmInstall()this.showProjectTips()}}

@yami-beta/create-ts-appでは このような実装になっています。
一部を抜粋すると、以下のようにコマンド実行時の回答に応じて package.json に記載する依存関係を編集することも可能です。

constconfig={actions(){const{answers}=this;return[// 略{type:"modify",files:"package.json",handler(data:any,filepath:string){return{name:answers.name||data.name,version:answers.version||data.version,main:data.main,author:answers.author,license:answers.license||data.license,scripts:data.scripts,dependencies:{...data.dependencies},devDependencies:{...data.devDependencies,"@typescript-eslint/eslint-plugin":answers.features.includes("eslint")?data.devDependencies["@typescript-eslint/eslint-plugin"]:undefined,"@typescript-eslint/parser":answers.features.includes("eslint")?data.devDependencies["@typescript-eslint/parser"]:undefined,eslint:answers.features.includes("eslint")?data.devDependencies["eslint"]:undefined,"eslint-config-prettier":answers.features.includes("eslint")&&answers.features.includes("prettier")?data.devDependencies["eslint-config-prettier"]:undefined,"eslint-plugin-prettier":answers.features.includes("eslint")&&answers.features.includes("prettier")?data.devDependencies["eslint-plugin-prettier"]:undefined,prettier:answers.features.includes("prettier")?data.devDependencies["prettier"]:undefined}};}},// 略].filter(Boolean);}};

CLI を作るほどでもない場合

ボイラープレートは欲しいけれども CLI を作るほどでは無い、という場合もあるかと思います。
そういう場合は GitHub のテンプレートリポジトリでボイラープレートを活用する方法があります。

詳細は上記のドキュメントを参照してください。

まとめ

  • React アプリケーションのボイラープレートを生成する CLI を作っている
    • テンプレートからファイルをコピー、リネーム、編集することが出来るので複数のボイラープレート生成が可能
  • 手軽にボイラープレートを作る場合は GitHub のテンプレートリポジトリが活用出来そう

備考

  • SAOという見覚えのある名前ですが egoist氏のライブラリです

エンジニアの作業内容をRedashで可視化してみた

$
0
0

ようへいです。

TuneCore Japan (https://www.tunecore.co.jp/) という、
音楽系Webサービスでエンジニアをしています。

今回はタイトルの通り、エンジニアの作業内容をRedashで可視化する話です。
Redashについては後述しますが、簡単に言うと可視化ツールの一つです。

実際のRedash画面

以下は、実際に運用しているRedash画面の一部です。

スクリーンショット 2019-11-21 10.42.59.png

何やらカラフルなグラフが出ていますね。

細かな項目については、業務内容のため公開できないのですが、
ざっくり言うと、誰が何のタスクを何時間やったのかを、グラフ化しています。

他にも、Redashでは表(テーブル)や円グラフなど、様々な形態で表示できるので、
目的に応じて使い分けています。

また、CSVやExcelでダウンロードもできるので、マネージャ等の管理側のメンバーからエクセルファイルを求められるときにも対応できます。

使っているツール

使っているツールは以下の通りです。

JIRA

https://www.atlassian.com/ja/software/jira
タスク管理サービス。
似ているサービス:Redmine、Backlogなど

Tempo Timesheets

https://marketplace.atlassian.com/apps/6572/tempo-timesheets?hosting=cloud&tab=overview
上記のJIRAの有料アドオン。
作業時間を記入して、それを一覧で見ることができる。

Redash

https://redash.io/
オープンソースのBIツール。(BI: Business Intelligence)
MySQLなどのデータベースの中身を簡単にグラフ化したりできる。
似ているサービス:Metabase、Kibanaなど

MySQL

データベース。
詳細は省略します。

具体的な運用

現時点では、月ごとのサイクルで、以下のような運用をしています。
とてもシンプルかと思います。

月初のタイミング

先月分の作業内容・時間をCSVダウンロードして、MySQLにインサートする

毎日(営業日)

エンジニアは、作業時間をJIRA(Tempo Timesheets)に入力する

月末のタイミング

翌月分のスケジュール(作業内容・時間)のCSVを作成し、MySQLにインサートする
※ スケジュールに対して、どれくらいズレたかを計測するため

作業時間の記入漏れ問題

上記の運用では、毎日作業時間を入力する必要があるのですが、正直忘れがちです。
そこで、先週分の作業時間に漏れがないかを一週間に一回チェックする
Slackのボットを作りました。

以下が、実際にボットから通知が来た時の、画面の一部です。
スクリーンショット 2019-11-21 10.54.13.png

サイレンの右の部分には、yohei: 30hのように名前と時間が書いてあり、
サイレンの色によって漏れ具合を表現するようにしました。
ちなみに、こちらはnodejsを使って、Tempo TimesheetsのAPIを実行して、Slackに流し込んでいます。

このSlackボットのおかげで、記入漏れが減ったとか、減っていないとか...?
もう少し、様子見ですね。

まとめ

ということで、エンジニアの作業内容をRedashで可視化してみました。

しかしながら、本来の目的は可視化ではなく、
可視化されたデータからどう意思決定するかです。

実際に、チーム内でも、
「XXXのタスクに時間がかかり過ぎている」
「YYYのタスクに全く時間が割けていない」
などの問題が、あぶり出されてきました。

来年はこの問題を解決して、開発スピードを上げていきたいと思っている所存です。

参考になれば幸いです!

トーク Bot とのトークルームをたくさん作る

$
0
0

LINEWORKS Advent Calendar 2019 の 5日目を担当させていただきます!
どうぞよろしくおねがいしますm( )m

最近見つけた、ちょっとした小ネタを紹介しますね~。

トーク Bot とのトークルームをたくさん作る

タイトルの通りなのですが、トーク Bot とのトークルームをたくさん作ります。
つまり、こんな感じ。
1574306918.png
仕組みとしては単純です。
1:1トークルームではなくて、Bot と2人きりの1:N トークルームを作っているだけです。
なので、この方法を使うときは DeveloperConsole で Bot の Bot ポリシーの「複数人のトークルームに招待可」に必ずチェックを入れておいてくださいね(*'▽')

Bot とのトークルームの作り方

トークルームの作り方はみなさんご存じだと思いますので、さささっと説明しちゃいますが、
トーク画面の左上の追加ボタンを押すと Bot を招待することができます。

1574300569.png

このとき、Bot を1つしか選ばないと1:1のトークルームになってしまうので、別の Bot も一緒に招待します。
それで、1:N のトークルームが出来上がったらいらない方の Bot をトークルームから退出させます!
1574311849.png
1574312222.png

('Д')ドイヒー

そうすると Bot 二人きりのトークルームになるので、これを繰り返すと Bot とのトークルームをたくさん作ることができます。

( ^ω^)・・・うん、めんどくさいですね!

API を使って、Bot とのトークルームをたくさん作る

面倒なことは自動でやれるようにすればいいのです。
Bot を含むトークルーム作成 APIメッセージ送信 APIを駆使して一気に作っちゃいましょう!(^^)/

node.js でのコードになります。
あらかじめ request-promiseモジュールをインストールしておいてくださいませ。

> npm install request-promise

API ID などの Key はご自身のものを入力してください。

makeManyTalkRooms.js
constrequest=require("request-promise");constapiId="API ID";constbotNo="部屋をいっぱい作りたい Bot の botNo";constheaders={Authorization:"Bearer "+"Token",consumerKey:"consumerKey","Content-Type":"application/json"};consturl="https://apis.worksmobile.com/r/"+apiId+"/message/v1/bot/"+botNo;consttitles=["room A","room B","room C","room D","room E"];titles.forEach((title)=>{letoptions={url:url+"/room",headers:headers,json:{accountIds:["accountId"],title:title}};request.post(options).then((body)=>{letoptions={url:url+"/message/push",headers:headers,json:{roomId:body.roomId,content:{type:"text",text:"To "+title}}};request.post(options).catch((error)=>{thrownewError(error)});}).catch((error)=>{thrownewError(error)});});

トークルーム名は 11 行目の titlesに格納されています。
titlesの要素を増やせばいくらでもトークルームを作ることが可能です!ヾ(´∀`)ノ

トークルームをたくさん作ることによってできること

言っておいてなんなのですが、あまり思いつきませんね。。。
ルームを分けるということは、処理によって対応を分ける必要があるということだと思うのですけど、それなら Bot を複数作った方がわかりやすいですよね。
Bot の利用範囲の制限もできるし、メンバーのメンテナンスも LINEWORKS のメンバー管理と紐づけられるし。

ひとつだけ思いついたのは、外部ユーザへの問い合わせ対応的なものでしょうか?
外部ユーザとのトークルームには LINEWORKS Bot を招待できないので。
例えば、LINE 公式アカウントにトークすると担当者の LINEWORKS に専用トークルームが作られて、そこへトークされる。
そのトークルームに担当者が返事を書くと、LINE 公式アカウントが LINE ユーザに返事をする。
みたいな?

1574321110.png

  • LINE ユーザは LINE 公式アカウントへ話しかける
  • LINE 公式アカウントへのトーク内容を LINEWORKS Bot が LINEWROKS 担当者へトークする
  • そのとき、個別のトークルームにトークする(例:ユーザ A のトークはルーム A に送信)
  • LINE の userId と LINEWORKS の roomId を紐づかせて保持しておく
  • 個別のトークルームでのトークは紐づいた LINE ユーザへ送信される(例:ルーム A のトークはユーザ A に送信)

メリットとしては、担当者の匿名性が保てることでしょうか。
あとは、担当者が複数居た場合に、誰が返事をしても公式アカウントからの返答になることですかね。

んー、どんなシチュエーションですかね。
この仕組みでサービスとして実現できたら、ぜひ教えてくださいm( )m

おわりに

ここまでお付き合いいただきありがとうございました。

そして、LINEWORKS Advent Calendar 2019 の 5日目として参加させていただきありがとうございました。

Advent Calendar の後半、まだ空いてるから今回の仕組み、作ってみようかな~。
間に合わないから無理かな~( ゚Д゚)

ではまた!(^^)/

参考にさせていただきましたm(_ _)m

LINEWORKS Developers

Node.jsでMySQLに接続するのをDockerでやってみた

$
0
0

この記事は富士通クラウドテクノロジーズ Advent Calendar 2019 5日目の記事です。
4日目は @tmtmsさんの MySQL Parameters を拡張したでした。

はじめに

  • ※本記事は、 Node.js超入門[第2版]のサンプルコードをDocker上で動かす趣旨のものです。 よって、書籍で言及されている階層は触れません。

はじめまして。Node.jsを勉強中の新卒エンジニアです。
配属したてホヤホヤです。
今回は Node.js超入門[第2版]を使って勉強したときの話をします。

この書籍の内容をDockerで実現しようとした経緯ですが、そのまま「Dockerを理解したかったから」です。
弊社の研修でもDockerについて触れていただいたのですが、一度教わっただけでは理解が難しいものでした。
というわけで、サンプルコードを写経するだけになりがちだった(主観です)プログラミングの本で、ついでに理解を深めようという魂胆です。

今回は、書籍の Chapter5「値とデータをマスターしよう!」の、Section5-3~5-4 で書かれている、Node.jsでDBにアクセスして情報を取得し、ブラウザに表示するプログラムを紹介します。書籍が手元にある方はぜひ確認してみてください。

環境構築

必要な環境

参考記事:

解説

  • 今回は、Node.jsもMySQLも、Dockerコンテナとして生成しています。
  • Node.jsサーバーは、 docker-compose.ymlから Dockerfileを呼び出す形で書き、MySQLサーバーは docker-compose.yml内で定義しています。
  • docker-compose.ymlで指定するMySQLのDockerイメージは、MySQLのバージョンが8よりも前になるように古いものを使っています。 8以降はそのままだとNode.jsとの連携ができないようです。
  • init-mysql.shは、ローカル環境から、データベース用のコンテナにアクセスし、テーブル作成->データ挿入までを行っています。
  • 実は、docker/db/my.cnfdocker-compose.ymlの2か所で mysqldを設定しているのですが、どちらかを消すとなぜかエラーになってしまうため(!)そのままにしてあります。いい方法ががありましたら教えていただけると幸いです・・・。
  • 余談ですが、今回のコードは、いままで述べてきたものとは別のDockerfileでExpress-generatorを実行し、 docker cpでローカルにファイルをコピーしてから作成していきました。 めんどうくさいことをしたなと自分で思います。

動作確認

  • 上記のリポジトリをcloneします。
  • リポジトリに移動
  • docker-compose up -dでコンテナを立ち上げます。
  • すこし(10秒ほど)時間をおきます。(コンテナが出来上がるまで待つ)
  • ローカルで ./init-mysql.shを実行します。
  • localhost/helloにアクセスし、 docker/db/sql/002-insert-records.sqlで入れたデータが表示されていることを確認してください。
  • ↓ こんなふうに表示されます。

image.png

おわりに

まず、Node.jsについての書籍を読み、サンプルを実行してみることで、Node.jsの挙動についてざっくりと理解できるようになりました。
加えて、それらをDocker上の環境で行うことで、
いままで「VirtualBoxより軽い仮想化するアプリケーション?」という認識だったDockerについての理解も深まりました。
Linuxのコンソールの扱いも以前よりわかるようになってきました。
やっと .profile.vimrcをカスタマイズできるようになり、快適なUbuntu生活をしております。

また、試行錯誤して、調べたり人に聞いたりしていくことが学びへの近道だと実感しました。
例えば、MySQLのバージョンに関しては自分では調べきれず、先輩方の知恵をお借りしました。
ありがとうございました。

まだまだエンジニアとしては若輩者ですが、これからもっと勉強してスキルをつけ、開発をたくさんしていけたらと思います。

discord.jsでDiscord Botを作ってみた

$
0
0

概要

discord.jsでbotを作ったので、それを書き留めときます。
インストールは、Ubuntu mateを想定したものを紹介致します。
他のOSを使っている方に関しては、調べてください

Node.js&npmのインストール方法

コマンドラインを開き下のコマンドを実行します。

sudo apt install -y nodejs npm

discord.jsのインストール方法

こちらもコマンドラインを開き下のコマンドを実行します。

sudo npm i discord.js

実際に書いていく

まずは、discord.jsを読み込むコードを書きます。

index.js
constdiscord=require("discord.js");constclient=newdiscord.Client;

簡単な返事機能のコードを書きます。

index.js
//続きclient.on("message",message=>{if(message.author.bot||!message.guild)returnif(message.content==="こんにちは"){message.reply('さん、こんにちはー');}});client.login("Botのトークン")

Botのトークンのところには、Discord Developer Portal
にある、自分のBotの右にあるBotへ行きtokenの下にあるcopyを押して、index.jsのBotのトークンのところへ貼り付けます。

実際にBotを起動してみる

Botを起動させるためにはコマンドラインで下のコマンドを実行します。

node index.js

ちゃんとBotがONLINEになっていれば成功です。
色々な種類のコードを書いていく予定ですので、よろしくお願いします。

Unityの.alfファイルから自動で.ulfをダウンロードしたい!

$
0
0

CIのActivateとかでライセンスを自動でActivateさせたい!

CIでUnityを扱う時はJenkinsとかであれば問題ないのですが、CircleCIやGitHub Actionsを使用するときにDockerでのライセンス認証では.ulfファイルというのが必要になってきます。

現在.ulfファイルをコマンドラインから生成することはできません。生成するにはブラウザ経由の一択です。
それを今回Puppeteerというnode.jsのツールを使って自動化してみました。

※今回の認証フローはPersonalEdition固定になります。

Puppetterとは、Webブラウザでの操作をソースコードから行えるものになります。
詳しくはこちら
Puppeteer

今回のリポジトリはこちら
MizoTake/unity-license-activate

実装

npm経由でPuppeteerを入れて以下のjsで実装しました。

今回はライセンスの認証が必要になるので https://license.unity3d.com/manualのページで操作を行います。
手元にあるalfファイルから最終的にulfファイルをダウンロードする操作になります。

叩くコマンドは以下になります
node activate.js $email $password $alf_file_path

以下が今回のScriptの全容ですが細かく分けてどうなっているのか下で記述します。

activate.js
constpuppeteer=require('puppeteer')constfs=require('fs');(async()=>{constbrowser=awaitpuppeteer.launch()constpage=awaitbrowser.newPage()constdownloadPath=process.cwd()constclient=awaitpage.target().createCDPSession()awaitclient.send('Page.setDownloadBehavior',{behavior:'allow',downloadPath:downloadPath})awaitpage.goto('https://license.unity3d.com/manual')awaitpage.waitForNavigation({timeout:60000,waitUntil:'domcontentloaded'})constemail=`${process.argv[2]}`awaitpage.type('input[type=email]',email)constpassword=`${process.argv[3]}`awaitpage.type('input[type=password]',password)awaitpage.click('input[name="commit"]')awaitpage.waitForNavigation({timeout:60000,waitUntil:'domcontentloaded'})constinput=awaitpage.$('input[name="licenseFile"]')constalfPath=`${process.argv[4]}`awaitinput.uploadFile(alfPath)awaitpage.click('input[name="commit"]')awaitpage.waitForNavigation({timeout:60000,waitUntil:'domcontentloaded'})constselectedTypePersonal='input[id="type_personal"][value="personal"]'awaitpage.evaluate(s=>document.querySelector(s).click(),selectedTypePersonal)constselectedPersonalCapacity='input[id="option3"][name="personal_capacity"]'awaitpage.evaluate(s=>document.querySelector(s).click(),selectedPersonalCapacity)awaitpage.click('input[class="btn mb10"]')awaitpage.waitForNavigation()awaitpage.click('input[name="commit"]')let_=await(async()=>{letulfdo{for(constfileoffs.readdirSync(downloadPath)){ulf|=file.endsWith('.ulf')}awaitsleep(1000)}while(!ulf)})()functionsleep(milliSeconds){returnnewPromise((resolve,reject)=>{setTimeout(resolve,milliSeconds)})}awaitbrowser.close()})()

画面ごとの処理

1.png

// ライセンス認証を行うページに行くawaitpage.goto('https://license.unity3d.com/manual')awaitpage.waitForNavigation({timeout:60000,waitUntil:'domcontentloaded'})// ライセンス認証を行うページに飛ばしたがリダイレクトでUnityのログインページに飛んでいる//コマンド引数からメールアドレスとパスワードをとってくるconstemail=`${process.argv[2]}`awaitpage.type('input[type=email]',email)constpassword=`${process.argv[3]}`awaitpage.type('input[type=password]',password)// Sign inのボタンを押すawaitpage.click('input[name="commit"]')

2.png

constinput=awaitpage.$('input[name="licenseFile"]')// コマンドライン引数で指定したpathからfileを添付constalfPath=`${process.argv[4]}`awaitinput.uploadFile(alfPath)// Nextボタンを押すawaitpage.click('input[name="commit"]')

3.png

これでファイル添付ができてることがわかります。ファイル添付までできるのすげぇ…Puppeteer…

4.png

// Personal Editionの選択constselectedTypePersonal='input[id="type_personal"][value="personal"]'awaitpage.evaluate(s=>document.querySelector(s).click(),selectedTypePersonal)// Personal Edition選択後に出てくるのOptionを選択constselectedPersonalCapacity='input[id="option3"][name="personal_capacity"]'awaitpage.evaluate(s=>document.querySelector(s).click(),selectedPersonalCapacity)   // Nextボタンを押すawaitpage.click('input[class="btn mb10"]')

この画面の実行後はこうなっています。
5.png

6.png

// Download license fileボタンを押すawaitpage.click('input[name="commit"]')// ダウンロードが始まるので手元に.ulfファイルができるまで待つlet_=await(async()=>{letulfdo{for(constfileoffs.readdirSync(downloadPath)){ulf|=file.endsWith('.ulf')}awaitsleep(1000)}while(!ulf)})()functionsleep(milliSeconds){returnnewPromise((resolve,reject)=>{setTimeout(resolve,milliSeconds)})}

以上のような流れになっています。

さいごに

Puppeteer便利!!!!

awaitpage.screenshot({path:"./example.png"});

でScreenShotを撮りつつ実装してました。

ブラウザでしか行えない操作もこれがあればできるので色々と捗るんじゃないかなと思っています。


C10K問題とNode.js

$
0
0

C10K問題(クライアント1万台問題)

  • アクセスするクライアント数が1万を超えると、サーバーのスレッド(並列処理の単位)数が増え、サーバーのメモリーなどのリソースが不足してしまう問題
  • 処理能力に余裕があっても、クライアントの数が多くなると効率が悪化しサーバがパンクする
  • プロセッサの処理能力には余裕があっても、サーバの台数を増やさなければいけなくなってしまう

回避方法

  • サーバーサイドではイベント駆動方式を利用しているNode.jsなどを使用する
    • イベント駆動により大量のリクエストを同時に処理できるスケーラビリティを備えている
    • ノンブロッキングI/Oモデルにより、C10K問題に対応する

Node.jsとは

スケーラブルなネットワークアプリケーションを構築するために設計された非同期型のイベント駆動のJavaScript環境
Node.js

  • それぞれの意味
    • スケーラブル : 拡張性が高い
    • 非同期 : 各要求(request)の処理が完了するのを待たずに、それ以降の処理を行う方式
    • イベント駆動 : イベントと呼ばれるアプリや端末上で起きた出来事に対して処理を行うプログラムの実行形式
  • 特徴
    • サーバーサイドで使用できる
    • ノンブロッキングI/Oモデルを採用しており、I/Oの処理を待たずに次の処理を始めることができるので、大量のデータ処理が可能
      • ノンブロッキング : ある処理を行いながら、ほかの処理も同時進行で行えること
      • I/O : Input/Outputの略で、入出力の意

参照

Firebaseをなるべく安い料金で頑張りたい人へ

$
0
0

この記事はFirebaseアドベントカレンダーの5日目の記事です。

どうも!ハムカツおじさんという名前でtwitterやってます(@hmktsu)🤘
自分でだったり弊社でだったりなどFirebaseを使ってサービスを作っています。

ちなみに先日Firebase Meetup #15 Cloud Functions Dayにて下記のスライドを発表させていただいたので、興味ある人はご覧になってください!
Firebaseオンリー + React Nativeでアプリを作ると果たして簡単になるのか?

はじめに

色んなツールを作るのにFirebaseを使うととても便利だなぁという感想を持っています。
料金に関しては体感としてですが常時サーバを立ち上げてるのとは違いお安くなります。
それでももっと安く済ませたいとかそういったことはあるのでしょうか。

本番環境でそれなりにお金がかかるのはしょうがないんだけども、テスト環境ではなるべくお金かけたくないしなぁ〜ということもあるのではないでしょうか。

ということでここらへんを気をつければなるべく料金を抑えれるんじゃないかなということを今回は紹介させていただきます。

対象とするFirebaseのサービス

  • Firestore
  • Functions
  • Hosting

ざっくり気をつけたポイント

Firestore

  • リージョンをどうするか
  • ドキュメントの読み出し/書き込み/削除
  • ページングの方法
  • セキュリティールールについて

Functions

  • メモリ(CPU)の割り当て
  • 料金プラン

Hosting

  • どういったファイルを置いているのか

こういう風に工夫するとよいかも

Firestore

  • USじゃないといけない理由がないならば他のリージョンを使う
    • USはマルチリージョンなのでちょっと他よりもお高め(地域別料金)
  • このデータが入っているか?という確認をしたいときに、コレクションからwhereするのではなく、複数のドキュメントを配列に格納したドキュメントを用意して、その中に入ってるかどうか比較する
  • 読み出しをする際にオフセットを使わないでカーソルを使う
  • ルールの評価はリクエストごとに1回のみ課金されるので、複数ドキュメントを呼び出すときはえいやと思い切って呼び出してみる(Cloud Firestore セキュリティ ルール)

Functions

  • メモリ量でGB秒やCPU秒の料金が変わるので、さっと終わる処理ならばメモリを小さくする(コンピューティング時間)
  • Blazeプランを使うと料金プランページに表示されているSparkプラン以上に無料で使える範囲が広くなる

Hosting

  • Storageと比べると転送量や保存量が割高なので、Storageに画像とかをちゃんとおく

まとめ

正直なところFirebaseはAWSなどで組むのと違ってあまりお金はかかりません。
それにスケーラブルなので落ちないという利点もあります。

ただその代わりこの記事みたいに落ちないからこそ、使い方などを間違えてしまうと異常な料金になってしまうこともあります。
地味によくある話としてはStorageを使わずにHostingのみでWebサイトを作り、バズってしまったがために中々な請求金額がきてしまうとか。

ちなみに最終手段ですが、1日の使用料を実は制限することができます。
また1ヶ月の予算を設定して使用料に応じてアラートを出すこともできます。
1日あたりの費用制限を設定する
1か月の予算を設定する

これらを使うこともある意味ありなんじゃないかなと思います。

React で eject せずに Scoped SASS (.scss) を使う

$
0
0

概要

  • scoped sass (ファイル内限定で適用されるスタイル) を使いたいでござる
  • でもnpm run ejectはしたくないでござる
  • cra-sassを導入するとかんたんにできるでござる

参考文献

実行環境

  • create-react-appで作った react project
    • 既存プロジェクトなのでversionわからん すまん
  • TypeScript

サンプルコード (変更前)

node-sassを入れてふつーにscssを使うとこうなる。

Sample.tsx

Sample.tsx
import*asReactfrom"react";import"./Sample.scss";exportconstSample:React.FC=()=>{return(<divclassName="outer">
      OUTER
      <divclassName="inner">INNER</div><ul>{["red","blue","green"].map((each,index)=>(<liclassName={each}key={index}>{each.toUpperCase()}</li>))}</ul></div>);};exportdefaultSample;

Sample.scss

Sample.scss
.outer{&,*{display:flex;flex-direction:column;padding:8px;border-left:1pxsolidgray;}font-size:1.2rem;.inner{font-weight:bold;}ul{li{&.red{color:red;}&.green{color:green;}&.blue{color:blue;}}}}

ビルド結果(html)

<divclass="outer">
  OUTER
  <divclass="inner">INNER</div><ul><liclass="red">RED</li><liclass="blue">BLUE</li><liclass="green">GREEN</li></ul></div>

実行結果

この実装の問題点

Sample.scss に記述したスタイルのscopeはグローバルである。
すなわち、Sample.tsx と同時にロードされるコンポーネントに、
同じclassName(例えば.outer)が割りあたっていると、互いに影響を受け合いバグの原因となる

解決策

閉じたscopeを扱うことのできるsass loaderを導入する

導入手順

cra-sass を導入

npm install --save-dev cra-sass

cra-sass を実行

$(npm bin)/cra-sass

するとなんかいっぱいインストールしてプロジェクトが魔改造される

package.json
@ devDependencies
+    "cra-sass": "0.0.5",

@ dependencies
+    "node-sass-chokidar": "^1.4.0",
+    "npm-add-script": "^1.1.0",
+    "npm-run-all": "^4.1.5",

@ scripts
-    "start": "react-scripts start",
-    "build": "react-scripts --max-old-space-size=2048 build",
+    "start": "npm-run-all -p watch-css start-js",
+    "build": "npm run build-css && react-scripts build",
     "test": "react-scripts test",
-    "eject": "react-scripts eject"
+    "eject": "react-scripts eject",
+    "build-css": "node-sass-chokidar src/ -o src/",
+    "watch-css": "npm run build-css && node-sass-chokidar src/ -o src/ --watch --recursive",
+    "start-js": "react-scripts start"

--max-old-space-size=2048とか無くなってぶっ壊れてんじゃん!
ってことで無駄にぶっこわされたとこは直しておく

package.json
-    "build": "react-scripts --max-old-space-size=2048 build",
+    "build": "npm run build-css && react-scripts --max-old-space-size=2048 build",

サンプルコード(リファクタ後)

Sample.scss

Sample.module.scssに改名する

Sample.tsx

  • scssのimport
  • classNameの割当てのしかた

だけを変更

Sample.tsx
import*asReactfrom"react";importstylesfrom"./Sample.module.scss";exportconstSample:React.FC=()=>{return(<divclassName={styles.outer}>
      OUTER
      <divclassName={styles.inner}>INNER</div><ul>{["red","blue","green"].map((each,index)=>(<liclassName={styles[each]}key={index}>{each.toUpperCase()}</li>))}</ul></div>);};exportdefaultSample;

ビルド結果

<divclass="Sample_outer__144wv">
  OUTER
  <divclass="Sample_inner__EiBfI">INNER</div><ul><liclass="Sample_red__1ktYQ">RED</li><liclass="Sample_blue__32ZOZ">BLUE</li><liclass="Sample_green__2OrZU">GREEN</li></ul></div>
css部分の抜粋
.Sample_outer__144wv{font-size:1.2rem;}.Sample_outer__144wv,.Sample_outer__144wv*{display:flex;flex-direction:column;padding:8px;border-left:1pxsolidgray;}.Sample_outer__144wv.Sample_inner__EiBfI{font-weight:bold;}.Sample_outer__144wvulli.Sample_red__1ktYQ{color:red;}.Sample_outer__144wvulli.Sample_green__2OrZU{color:green;}.Sample_outer__144wvulli.Sample_blue__32ZOZ{color:blue;}

その他やったこと

scriptsが壊されてないかチェックしよう

start, build, test が、 cra-sass によって破壊されている恐れがある
特にdefaultから変更している場合注意しよう

.cssが.scssと同階層に出力されるようになってうっおとしい

  • node-sass-chokidar のしわざくさい
  • でもoutputしなくするオプションとかなさげ
  • めんどいから、別階層に吐かせて、ignoreすることにした
package.json
-    "build-css": "node-sass-chokidar src/ -o src/",
+    "build-css": "node-sass-chokidar src/ -o built-css/",
-    "watch-css": "npm run build-css && node-sass-chokidar src/ -o src/ --watch --recursive",
+    "watch-css": "npm run build-css && node-sass-chokidar src/ -o built-css/ --watch --recursive",
.gitignore
+/built-css

node-sassをすでに使っていた場合、不要になる

npm r node-sass

おしまい

これで快適な React x Scoped SASS 生活が始まる

node.jsを触るために簡単なチャットシステムを作る(メッセージ送信編)

$
0
0

Node.js を触ってみたいと思ったので、備忘録も兼ねて以下に記します。
よりよい方法やバグ等ございましたら、アドバイスいただけると光栄です。

今回は「メッセージ送信編」ということで、クライアントからサーバーへのメッセージの送信の処理を作成します。

※前回 node.jsを触るために簡単なチャットシステムを作る(サーバー接続編)という表題で、サーバー接続の処理を作成していますので、まだな方はこちらを参照ください。

クライアントからサーバーへメッセージを送信する

メッセージ入力フォームを作成

/public/index.htmlに、以下のメッセージ入力フォームを追加します。

index.html
<formaction=""><inputtype="text"id="input_message"autocomplete="off"/><buttontype="submit">Send</button></form>

index.html全体としては、以下のようになります。

index.html
<!DOCTYPE html><html><head><metacharset="utf-8"><title>mychat</title></head><body><h1>node.js を触ってみた</h1><formaction=""><inputtype="text"id="input_message"autocomplete="off"/><buttontype="submit">Send</button></form><script src="/socket.io/socket.io.js"></script><script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script><script src="client.js"></script></body></html>

クライアント側の処理

/public/client.jsの末尾に、下記のようなSendボタンを押したときの処理を追加します。

client.js
// 「Send」ボタンを押したときの処理$('form').submit(()=>{const$inp=$('#input_message');consttext=$inp.val();console.log('#input_message :',text);if(text){// サーバーに、イベント名 'new message' で入力テキストを送信socket.emit('new message',text);// テキストボックスを空に$inp.val('');}// フォーム送信はしないreturnfalse;});

/public/client.js全体としては、以下のようになります。

client.js
'use strict';// クライアントからサーバーへの接続要求constsocket=io.connect();// 接続時の処理socket.on('connect',()=>{console.log('connect');});// 「Send」ボタンを押したときの処理$('form').submit(()=>{const$inp=$('#input_message');consttext=$inp.val();console.log('#input_message :',text);if(text){// サーバーに、イベント名 'new message' で入力テキストを送信socket.emit('new message',text);// テキストボックスを空に$inp.val('');}// フォーム送信はしないreturnfalse;});

サーバー側の処理

server.jsの「接続時の処理」の中に、下記処理を追加します。

server.js
// 新しいメッセージ受信時の処理socket.on('new message',(strMessage)=>{console.log('new message',strMessage);});

server.js全体としては、以下のようになります。

server.js
'use strict';// モジュールconsthttp=require('http');constexpress=require('express');constsocketIO=require('socket.io');constmoment=require('moment');// オブジェクトconstapp=express();constserver=http.Server(app);constio=socketIO(server);// 定数constPORT=process.env.PORT||3000;// グローバル変数letiCountUser=0;// ユーザー数// 接続時の処理io.on('connection',(socket)=>{console.log('connection');// 切断時の処理socket.on('disconnect',()=>{console.log('disconnect');});// 新しいメッセージ受信時の処理socket.on('new message',(strMessage)=>{console.log('new message',strMessage);});});// 公開フォルダの指定app.use(express.static(__dirname+'/public'));// サーバーの起動server.listen(PORT,()=>{console.log('server starts on port: %d',PORT);});

動作を確認する

サーバーを立ち上げた状態で、
http://localhost:3000にアクセスします。

「aaa」と「あああ」というメッセージをフォームに入力し、「Send」ボタンを押します。
Google Chrome のデベロッパーツールの Console に、connectに続いて、
#input_message : aaa
#input_message : あああ
と表示されます。

下記のようにサーバー側で connectionに続いて、
new message aaa
new message あああ
と表示されれば完了です。

node server.js
server starts on port: 3000
connection
new message aaa
new message あああ

サーバーからクライアントへメッセージを拡散する

サーバー側の処理

server.jsの「接続時の処理」の中に、下記処理を追加します。

server.js
// 送信元含む全員に送信io.emit('spread message',strMessage);

server.js全体としては、以下のようになります。

server.js
'use strict';// モジュールconsthttp=require('http');constexpress=require('express');constsocketIO=require('socket.io');constmoment=require('moment');// オブジェクトconstapp=express();constserver=http.Server(app);constio=socketIO(server);// 定数constPORT=process.env.PORT||3000;// グローバル変数letiCountUser=0;// ユーザー数// 接続時の処理io.on('connection',(socket)=>{console.log('connection');// 切断時の処理socket.on('disconnect',()=>{console.log('disconnect');});// 新しいメッセージ受信時の処理socket.on('new message',(strMessage)=>{console.log('new message',strMessage);// 送信元含む全員に送信io.emit('spread message',strMessage);});});// 公開フォルダの指定app.use(express.static(__dirname+'/public'));// サーバーの起動server.listen(PORT,()=>{console.log('server starts on port: %d',PORT);});

クライアント側の処理

/public/client.jsの末尾に、下記のようなSendボタンを押したときの処理を追加します。

client.js
// サーバーからのメッセージ拡散に対する処理socket.on('spread message',(strMessage)=>{console.log('spread message :',strMessage);// 拡散されたメッセージをメッセージリストに追加constli_element=$('<li>').text(strMessage);$('#message_list').prepend(li_element);});

/public/client.js全体としては、以下のようになります。

client.js
'use strict';// クライアントからサーバーへの接続要求constsocket=io.connect();// 接続時の処理socket.on('connect',()=>{console.log('connect');});// 「Send」ボタンを押したときの処理$('form').submit(()=>{const$inp=$('#input_message');consttext=$inp.val();console.log('#input_message :',text);if(text){// サーバーに、イベント名 'new message' で入力テキストを送信socket.emit('new message',text);// テキストボックスを空に$inp.val('');}// フォーム送信はしないreturnfalse;});// サーバーからのメッセージ拡散に対する処理socket.on('spread message',(strMessage)=>{console.log('spread message :',strMessage);// 拡散されたメッセージをメッセージリストに追加$('#message_list').prepend($('<li>').text(strMessage));});

メッセージをビュー側に表示

/public/index.htmlのメッセージ入力フォームの下に、以下のメッセージリストを追加します。

index.html
<ulid="message_list"></ul>

index.html全体としては、以下のようになります。

index.html
<!DOCTYPE html><html><head><metacharset="utf-8"><title>mychat</title></head><body><h1>node.js を触ってみた</h1><formaction=""><inputtype="text"id="input_message"autocomplete="off"/><buttontype="submit">Send</button></form><ulid="message_list"></ul><script src="/socket.io/socket.io.js"></script><script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script><script src="client.js"></script></body></html>

動作を確認する

メッセージの表示

サーバーを立ち上げた状態で、
http://localhost:3000にアクセスします。

「aaa」と「あああ」というメッセージをフォームに入力し、「Send」ボタンを押します。

送信したメッセージが、リスト表示されます。

別ブラウザでもメッセージを表示

ブラウザを別に立ち上げ、
http://localhost:3000にアクセスします。

「bbb」と「いいい」というメッセージをフォームに入力し、「Send」ボタンを押します。

送信したメッセージが、両方のブラウザに、即座に、リスト表示されれば完了です。

最後に

今回作成する機能としては以上となります。

ここで作成した機能をベースに、
以下のような機能や他にも自分で思いついた機能などを追加してみるのも良い学習になると思います。

  • メッセージに時刻を表示
  • メッセージに発信者名を表示
  • ユーザーの入室、退室を表示

ソースコードは以下に載せていますので、よろしければ参照ください。
https://github.com/genki-sano/express-socketio-chat

関連記事

堅牢な node.js プロジェクトのアーキテクチャとは?

$
0
0

こちらの記事は、Sam Quinn氏により2019年 4月に公開された『 Bulletproof node.js project architecture 』の和訳です。
本記事は原著者から許可を得た上で記事を公開しています。

GitHub repositoryでの実装例: 2019/04/21 アップデート

始めに

Express.jsは、node.js のREST APIを作成するための優れたフレームワークですが、node.jsプロジェクトの設計方法についての手がかりを与えてくれるものではありません。

ばからしく聞こえるかもしれませんが、この問題は確かに存在するのです。

node.jsプロジェクト構造の正しい設計により、コードの重複を回避でき、安定性を向上させます。また、正しく設計されていれば、サービスをスケールさせるときに役に立つかもしれません。

この記事は、貧弱な構造のnode.jsプロジェクト、望ましくないパターン、そしてコードリファクタリングと構造の改善に無数の時間を費やし対応してきた、長年の経験に基づく広範囲な探求です。

本記事に合わせnode.jsプロジェクトのアーキテクチャを見直すために助けが必要な場合は、santiago@softwareontheroad.comにご連絡ください。

目次

  • フォルダ構造
  • 3層アーキテクチャ
  • サービスレイヤー
  • Pub/Subレイヤー
  • Dependency Injection (DI) --※日本語で「依存の注入」
  • ユニットテスト
  • Cron ジョブと定期的なタスク
  • 構成情報及びシークレット
  • ローダー 例(GitHub repojitory)

フォルダ構造

以下はこれから話を進めていくnode.jsプロジェクトの構造です。

構築するすべてのnode.js REST APIサービスで、これをを使用します。では、それぞれのコンポーネントが何をするのか詳しく見ていきましょう。

│   app.js          # App entry point
  └───api             # Express route controllers for all the endpoints of the app
  └───config          # Environment variables and configuration related stuff
  └───jobs            # Jobs definitions for agenda.js
  └───loaders         # Split the startup process into modules
  └───models          # Database models
  └───services        # All the business logic is here
  └───subscribers     # Event handlers for async task
  └───types           # Type declaration files (d.ts) for Typescript

単なるJavascript ファイルの並び替えをする方法ではありません..

3層アーキテクチャ

下記のアイデアは、「関心の分離」の原則に基づき、ビジネスロジックをnode.js APIルーティングから分離させるものです。

これはあなたがいつか、CLIツールでビジネスロジックを使用したい、定期的なタスク処理では十分でない、と思うようになったときのためです。

そしてnode.jsサーバーからそれ自体へのAPI呼び出しは、良いアイディアではありません...

コントローラーにビジネスロジックを入れてはダメです!!

express.jsコントローラーを使用してアプリケーションのビジネスロジックを保存したくなるかもしれませんが、これはすぐにスパゲッティコードになります。ユニットテストを書く必要があるときには、リクエストまたはレスポンスexpress.jsオブジェクトの複雑なモックを扱うことになります。

いつ応答を送信するべきかを区別するのは複雑です。 バックグランドで処理が続行され、その後 応答がクライアントに送信されたとしましょう。

以下は望ましくない例です。

route.post('/',async(req,res,next)=>{// This should be a middleware or should be handled by a library like Joi.constuserDTO=req.body;constisUserValid=validators.user(userDTO)if(!isUserValid){returnres.status(400).end();}// Lot of business logic here...constuserRecord=awaitUserModel.create(userDTO);deleteuserRecord.password;deleteuserRecord.salt;constcompanyRecord=awaitCompanyModel.create(userRecord);constcompanyDashboard=awaitCompanyDashboard.create(userRecord,companyRecord);...whatever...// And here is the 'optimization' that mess up everything.// The response is sent to client...res.json({user:userRecord,company:companyRecord});// But code execution continues :(constsalaryRecord=awaitSalaryModel.create(userRecord,companyRecord);eventTracker.track('user_signup',userRecord,companyRecord,salaryRecord);intercom.createUser(userRecord);gaAnalytics.event('user_signup',userRecord);awaitEmailService.startSignupSequence(userRecord)});

ビジネスロジックをサービスレイヤーで扱っている

このレイヤーは、ビジネスロジックが存在すべき場所です。

それは、node.jsに適用されるSOLID原則に従って、明確な目的(情報)を持つクラスのコレクションです。

このレイヤーには「SQLクエリ」のいかなるフォームも存在するべきではありません。データアクセス層を使用してください。

  • express.jsルーターからソースコードを遠ざける
  • リクエストまたはレスポンスオブジェクトをサービスレイヤーに渡さない
  • ステータスコードやヘッダーなど、HTTPトランスポートレイヤーに関連するものをサービスレイヤーから返さない

route.post('/',validators.userSignup,// this middleware take care of validationasync(req,res,next)=>{// The actual responsability of the route layer.constuserDTO=req.body;// Call to service layer.// Abstraction on how to access the data layer and the business logic.const{user,company}=awaitUserService.Signup(userDTO);// Return a response to client.returnres.json({user,company});});

サービスが裏でどのように機能するかを以下に示します。

importUserModelfrom'../models/user';importCompanyModelfrom'../models/company';exportdefaultclassUserService{asyncSignup(user){constuserRecord=awaitUserModel.create(user);constcompanyRecord=awaitCompanyModel.create(userRecord);// needs userRecord to have the database id constsalaryRecord=awaitSalaryModel.create(userRecord,companyRecord);// depends on user and company to be created...whateverawaitEmailService.startSignupSequence(userRecord)...domorestuffreturn{user:userRecord,company:companyRecord};}}

Pub/Sub レイヤーも利用する

pub / subパターンは,、ここで提案されている従来の3層アーキテクチャを超えていますが、非常に便利です。

すぐにユーザーを作成できるシンプルなnode.js APIエンドポイントは、分析サービスであったり、あるいは電子メールシーケンスの開始などのサードパーティサービスを呼び出そうとするかもしれません。

遅かれ早かれ、そのシンプルな「作成」の操作はいくつかのことを実行し、1,000行にも及ぶコードがすべて1つの関数中で実行されることになるでしょう。

それは単一責任の原則に反しています。

したがって最初から責任を分離しておくほうが良く、それによってコードの保守性を維持できます。

importUserModelfrom'../models/user';importCompanyModelfrom'../models/company';importSalaryModelfrom'../models/salary';exportdefaultclassUserService(){asyncSignup(user){constuserRecord=awaitUserModel.create(user);constcompanyRecord=awaitCompanyModel.create(user);constsalaryRecord=awaitSalaryModel.create(user,salary);eventTracker.track('user_signup',userRecord,companyRecord,salaryRecord);intercom.createUser(userRecord);gaAnalytics.event('user_signup',userRecord);awaitEmailService.startSignupSequence(userRecord)...morestuffreturn{user:userRecord,company:companyRecord};}}

依存サービスへの呼び出し命令は、最良の方法ではありません。

ここでより良いアプローチは、イベントを発行することです。(例.「ユーザーはこのメールでサインアップしました」)

これで完了です。リスナーの仕事は、リスナーの責任としています。

importUserModelfrom'../models/user';importCompanyModelfrom'../models/company';importSalaryModelfrom'../models/salary';exportdefaultclassUserService(){asyncSignup(user){constuserRecord=awaitthis.userModel.create(user);constcompanyRecord=awaitthis.companyModel.create(user);this.eventEmitter.emit('user_signup',{user:userRecord,company:companyRecord})returnuserRecord}}

イベントハンドラー/リスナーを複数のファイルに分割できています。

eventEmitter.on('user_signup',({user,company})=>{eventTracker.track('user_signup',user,company,);intercom.createUser(user);gaAnalytics.event('user_signup',user);})
eventEmitter.on('user_signup',async({user,company})=>{constsalaryRecord=awaitSalaryModel.create(user,company);})
eventEmitter.on('user_signup',async({user,company})=>{awaitEmailService.startSignupSequence(user)})

awaitステートメントをtry-catchブロックにラップする、もしくは単に失敗処理として” unhandledPromise “プロセスとして処理することもできます。

依存性の注入 (D.I.)

依存性の注入(D.I.)、または制御の反転(IoC)は、クラスまたは関数の依存関係をコンストラクターに「注入」または渡すことで、コードの編成に役立つ一般的なパターンです。

このようにすることで、例えばサービスの単体テストを作成するときや、サービスが別のコンテキストで使用されるとき、「互換性のある依存関係」を注入する柔軟性が得られます。

D.I. なしのコード

importUserModelfrom'../models/user';importCompanyModelfrom'../models/company';importSalaryModelfrom'../models/salary';classUserService{constructor(){}Sigup(){// Caling UserMode, CompanyModel, etc...}}

手動でD.I. を実装したコード

exportdefaultclassUserService{constructor(userModel,companyModel,salaryModel){this.userModel=userModel;this.companyModel=companyModel;this.salaryModel=salaryModel;}getMyUser(userId){// models available throug 'this'constuser=this.userModel.findById(userId);returnuser;}}

これでカスタマイズされた依存関係を注入できます。

importUserServicefrom'../services/user';importUserModelfrom'../models/user';importCompanyModelfrom'../models/company';constsalaryModelMock={calculateNetSalary(){return42;}}constuserServiceInstance=newUserService(userModel,companyModel,salaryModelMock);constuser=awaituserServiceInstance.getMyUser('12346');

サービスが持つことのできる依存関係の量は無限で、新しく追加する際にいちいちインスタンス化をリファクタリングするのは、退屈でエラーが発生しやすいタスクです。

そういうわけでDI フレームワークが作成されました。

これにより、クラスで依存関係を宣言し、そのクラスのインスタンスが必要な場合には、 'Service Locator'を呼び出すだけでよくなります。

typedi“を用いてnode.jsにDIをもたらすnpmライブラリの例を見てみましょう。

“ typedi “の使用方法の詳細については公式ドキュメントをご覧ください。

注意: typescript での例

import{Service}from'typedi';@Service()exportdefaultclassUserService{constructor(privateuserModel,privatecompanyModel,privatesalaryModel){}getMyUser(userId){constuser=this.userModel.findById(userId);returnuser;}}

ここでtypediはUserServiceが必要とする依存関係を解決します。

services/user.js
import{Container}from'typedi';importUserServicefrom'../services/user';constuserServiceInstance=Container.get(UserService);constuser=awaituserServiceInstance.getMyUser('12346');

サービスロケーター呼び出しの乱用はアンチパターンです

Node.jsのExpress.jsでDIを使用する
express.jsでDIを使用する
これがnode.jsプロジェクトアーキテクチャのパズルの最後のピースです。

ルーティングレイヤー

route.post('/',async(req,res,next)=>{constuserDTO=req.body;constuserServiceInstance=Container.get(UserService)// Service locatorconst{user,company}=userServiceInstance.Signup(userDTO);returnres.json({user,company});});

Awesome! 素晴らしいプロジェクトになりました!

とても整理されていて、「今すぐ何かをコーディングしたい!」という気持ちになりますね。

サンプルのレポジトリにアクセスする

単体テストの例

DI とこれらの設計パターンを使用することにより、単体テストは非常にシンプルになります。

リクエスト / レスポンス オブジェクトのモックや “ require … “ などの呼び出しを行う必要はありません。

例:サインアップユーザーメソッドの単体テスト

tests/unit/services/user.js
importUserServicefrom'../../../src/services/user';describe('User service unit tests',()=>{describe('Signup',()=>{test('Should create user record and emit user_signup event',async()=>{consteventEmitterService={emit:jest.fn(),};constuserModel={create:(user)=>{return{...user,_id:'mock-user-id'}},};constcompanyModel={create:(user)=>{return{owner:user._id,companyTaxId:'12345',}},};constuserInput={fullname:'User Unit Test',email:'test@example.com',};constuserService=newUserService(userModel,companyModel,eventEmitterService);constuserRecord=awaituserService.SignUp(teamId.toHexString(),userInput);expect(userRecord).toBeDefined();expect(userRecord._id).toBeDefined();expect(eventEmitterService.emit).toBeCalled();});})})

Cronジョブと定期的なタスク

ここまででビジネスロジックがサービスレイヤーにカプセル化されたので、Cronジョブから使用するのが簡単になりました。

node.js のsetTimeoutや、その他の原始的なコード実行を遅らせる方法に頼るのではなく、ジョブやデータベース内での処理を永続化するフレームワークを使用するべきです。

こうすることで、失敗したジョブの制御や、成功した人のフィードバックを得ることができます。
node.js.
別の記事で、これらのグッドプラクティスについて既に書いていますので、こちらのガイドを確認してください。

構成情報及びシークレット

node.jsにおいて研鑽された概念である「Twelve-Factor App」に従えば、 APIキーとデータベース文字列の対応情報を保存するもっとも良い方法は、dotenvを使用することです。

決してコミットしてはいけない .envファイルを配置すると(ただし、リポジトリにデフォルト値で存在する必要があります)、 npm パッケージのdotenv
.envファイルをロードし、変数を node.js のprocess.envオブジェクトに挿入します。

これでも十分かもしれませんが、もうワンステップ加えたいと思います。

npmパッケージの dotenv が 参照するディレクトリ(今回の例では /config)配下に" index.js "ファイルを配置し、.envファイルを読み込むことで 、変数を格納するオブジェクトを使用できます。これで構造とコードの自動補完を保持できます。

config/index.js
constdotenv=require('dotenv');// config() will read your .env file, parse the contents, assign it to process.env.dotenv.config();exportdefault{port:process.env.PORT,databaseURL:process.env.DATABASE_URI,paypal:{publicKey:process.env.PAYPAL_PUBLIC_KEY,secretKey:process.env.PAYPAL_SECRET_KEY,},paypal:{publicKey:process.env.PAYPAL_PUBLIC_KEY,secretKey:process.env.PAYPAL_SECRET_KEY,},mailchimp:{apiKey:process.env.MAILCHIMP_API_KEY,sender:process.env.MAILCHIMP_SENDER,}}

こうすることでprocess.env.MY_RANDOM_VARによってコード記述の氾濫を回避でき、自動補完によって環境変数の命名方法を知る必要がなくなります。

サンプルのレポジトリにアクセスする

ローダー

このパターンはW3Techマイクロフレームワークから取得しましたが、そのパッケージには依存していません。

このアイデアでは、node.jsサービスの起動プロセスをテスト可能なモジュールに分割することが可能です。

古典的なexpress.jsアプリの立ち上げ手順を見てみましょう

constmongoose=require('mongoose');constexpress=require('express');constbodyParser=require('body-parser');constsession=require('express-session');constcors=require('cors');consterrorhandler=require('errorhandler');constapp=express();app.get('/status',(req,res)=>{res.status(200).end();});app.head('/status',(req,res)=>{res.status(200).end();});app.use(cors());app.use(require('morgan')('dev'));app.use(bodyParser.urlencoded({extended:false}));app.use(bodyParser.json(setupForStripeWebhooks));app.use(require('method-override')());app.use(express.static(__dirname+'/public'));app.use(session({secret:process.env.SECRET,cookie:{maxAge:60000},resave:false,saveUninitialized:false}));mongoose.connect(process.env.DATABASE_URL,{useNewUrlParser:true});require('./config/passport');require('./models/user');require('./models/company');app.use(require('./routes'));app.use((req,res,next)=>{varerr=newError('Not Found');err.status=404;next(err);});app.use((err,req,res)=>{res.status(err.status||500);res.json({'errors':{message:err.message,error:{}}});});...morestuff...maybestartupRedis...maybeaddmoremiddlewaresasyncfunctionstartServer(){app.listen(process.env.PORT,err=>{if(err){console.log(err);return;}console.log(`Your server is ready !`);});}// Run the async function to start our serverstartServer();

ご覧のとおり、アプリケーションのこの部分は非常に煩雑化しています。

これに関して効果的な対処法は以下です。

constloaders=require('./loaders');constexpress=require('express');asyncfunctionstartServer(){constapp=express();awaitloaders.init({expressApp:app});app.listen(process.env.PORT,err=>{if(err){console.log(err);return;}console.log(`Your server is ready !`);});}startServer();

ここでローダーは、簡潔な目的を持つ単なる小さなファイルです

loaders/index.js
importexpressLoaderfrom'./express';importmongooseLoaderfrom'./mongoose';exportdefaultasync({expressApp})=>{constmongoConnection=awaitmongooseLoader();console.log('MongoDB Intialized');awaitexpressLoader({app:expressApp});console.log('Express Intialized');// ... more loaders can be here// ... Initialize agenda// ... or Redis, or whatever you want}

express ローダー

loaders/express.js
import*asexpressfrom'express';import*asbodyParserfrom'body-parser';import*ascorsfrom'cors';exportdefaultasync({app}:{app:express.Application})=>{app.get('/status',(req,res)=>{res.status(200).end();});app.head('/status',(req,res)=>{res.status(200).end();});app.enable('trust proxy');app.use(cors());app.use(require('morgan')('dev'));app.use(bodyParser.urlencoded({extended:false}));// ...More middlewares// Return the express appreturnapp;})

mongo ローダー

loaders/mongoose.js
import*asmongoosefrom'mongoose'exportdefaultasync():Promise<any>=>{constconnection=awaitmongoose.connect(process.env.DATABASE_URL,{useNewUrlParser:true});returnconnection.connection.db;}

ローダーの完全な例はこちらをご覧ください

最後に..

ここまでで、私達は実績のあるnode.jsプロジェクトストラクチャについて深く理解できました。要約すると下記のような内容でしたね。

  • 3層アーキテクチャを使用する
  • ビジネスロジックをexpress.jsコントローラーに入れない
  • PubSubパターンを使用してバックグラウンドタスクのイベントを発行する
  • 負担を減らすためDI を実装する
  • パスワード、シークレット、APIキーなどを漏らさないために構成マネージャーを使用する
  • node.jsサーバー構成を、個別にロードできる小さな- モジュールに分割する

リポジトリの例はこちらからご覧ください。

ちょっと待って!まだ続きがあります。

この記事を楽しんでいただけたら、他の有益な情報も見逃すことがないように、私のメーリングリストを購読することをお勧めします。

何かを売りつけるようなことはしません。約束します!

今後の投稿もお見逃しなく!きっと気に入ってくれると思います :)

この記事のような、すごい記事がたくさんあるので、是非私のブログに来てください。

翻訳協力

Original Author: Sam Quinn
Thank you for letting us share your knowledge!

この記事は以下の方々のご協力により公開する事が出来ました。
改めて感謝致します。
選定担当: @aoharu
翻訳担当: @upaldus
監査担当: @aoharu
公開担当: @posaune0423

私たちと一緒に記事を作りませんか?

私たちは、海外の良質な記事を複数の優秀なエンジニアの方の協力を経て、日本語に翻訳し記事を公開しています。
活動に共感していただける方、良質な記事を多くの方に広めることに興味のある方は、ぜひご連絡ください。
Mailでタイトルを「参加希望」としたうえでメッセージをいただく、もしくはTwitterでメッセージをいただければ、選考のちお手伝いして頂ける部分についてご紹介させていただく事が可能です。
※ 頂いたメッセージには必ずご返信させて頂きます。

ご意見・ご感想をお待ちしております

今回の記事は、いかがだったでしょうか?
・こうしたら良かった、もっとこうして欲しい、こうした方が良いのではないか
・こういったところが良かった
などなど、率直なご意見を募集しております。
いただいたお声は、今後の記事の質向上に役立たせていただきますので、お気軽にコメント欄にてご投稿ください。Twitterでもご意見を受け付けております。
みなさまのメッセージをお待ちしております。

【Node.js】zxcvbnを使ってパスワード強度をチェックする

$
0
0

「パスワード強度チェックするようなライブラリって何かあるのかな?」
と興味本位で調べてみたらzxcvbnというものが見つかったので、ご紹介。

zxcvbnとは

Dropbox社製のパスワード強度チェッカーです。
Node.js以外にも色々な言語に対応したライブラリが作られています。

dropbox/zxcvbn: Low-Budget Password Strength Estimation

準備

$ npm i zxcvbn

基本的な使い方

とりあえずhogehogeという文字列に対してパスワード強度をチェックしてみましょう。

constzxcvbn=require('zxcvbn');constresult=zxcvbn('hogehoge');console.log(result);

基本的な使い方はとても簡単ですね。
第2引数に入力を渡して更に細かい設定をすることも可能なようですが、今回は遊んでみたかっただけなので割愛させてください。

出力内容を見てみましょう。

{
  password: 'hogehoge',
  guesses: 20003,
  guesses_log10: 4.301095134950942,
  sequence: [{
      pattern: 'repeat',
      i: 0,
      j: 7,
      token: 'hogehoge',
      base_token: 'hoge',
      base_guesses: 10001,
      base_matches: [Array],
      repeat_count: 2,
      guesses: 20002,
      guesses_log10: 4.301073422940843
    }],
  calc_time: 3, # zxcvbnが計算するのにかかった時間(ミリ秒) あんまり気にしなくていい
  crack_times_seconds: {# パスワードが特定されるまでにかかる時間(秒)# 4種類の攻撃パターンごとの想定時間# 基本的には`offline_fast_hashing_1e10_per_second`の値だけ見ておけばいいかも
    online_throttling_100_per_hour: 720108,
    online_no_throttling_10_per_second: 2000.3,
    offline_slow_hashing_1e4_per_second: 2.0003,
    offline_fast_hashing_1e10_per_second: 0.0000020003
  },
  crack_times_display: {# パスワードが特定されるまでにかかる時間(わかりやすい形式)
    online_throttling_100_per_hour: '8 days',
    online_no_throttling_10_per_second: '33 minutes',
    offline_slow_hashing_1e4_per_second: '2 seconds',
    offline_fast_hashing_1e10_per_second: 'less than a second'},
  score: 1, # 0 ~ 4でパスワード強度を評価する
  feedback: {
    warning: 'Repeats like "abcabcabc" are only slightly harder to guess than "abc"',
    suggestions: ['Add another word or two. Uncommon words are better.',
      'Avoid repeated words and characters']}}

crack_times_secondsの4種類のパターンについては、公式ドキュメントには次のように記述してありました。

  • online_throttling_100_per_hour

online attack on a service that ratelimits password auth attempts.

  • online_no_throttling_10_per_second

online attack on a service that doesn't ratelimit,
or where an attacker has outsmarted ratelimiting.

  • offline_slow_hashing_1e4_per_second

offline attack. assumes multiple attackers,
proper user-unique salting, and a slow hash function
w/ moderate work factor, such as bcrypt, scrypt, PBKDF2.

  • offline_fast_hashing_1e10_per_second

offline attack with user-unique salting but a fast hash
function like SHA-1, SHA-256 or MD5. A wide range of
reasonable numbers anywhere from one billion - one trillion
guesses per second, depending on number of cores and machines.
ballparking at 10B/sec.

うーん、セキュリティって難しい!

参考

コマンドラインからスマートLEDランプ(TP-Link KL110/KL130)をつける

$
0
0

TP-Link社のWifiスマートLEDランプ KL110/KL130 を使って、同一ネットワーク内にあるmacbookからランプをコマンドライン制御します。

color-temp

このランプの良いところは、電灯のON/OFFだけでなく色温度や輝度を調節できる点です。
例えば

  • 暖色系の色で輝度を落として落ち着いた雰囲気で集中作業する
  • 手元でモノを探すときに輝度を上げる
  • 物理本を読むときに色温度を変える(Kindleの背景の白とセピアくらい紙の色味が変わる)

といったことが手元でできるのが良いです。

特に色温度を変えるとだいぶ色味が変わります。
↓実際にランプの色温度最低値と最高値で見比べた写真:2500K<--->9000K
color-temp

こういった色味、明るさ、薄暗さをそのときの気分でコマンドラインで調整できるようにします。

試してみた環境

macbook : macOS Mojave
Node.js : v12.12.0

準備として、最初にスマホアプリでKL110/KL130のWifi設定を済ませておきます。
そして、同一ネットワーク内にmacbookを用意して
macbook ---- Wifiルータ ---- KL110/KL130
という構成にしておきます。
ちなみにKL110/KL130は2.4GHz Wifiにしか対応していないようです。(5GHz未対応)

tplink-lightbulb をインストール

tplink-lightbulbをnpmでグローバルインストールしておきます。
すると tplightコマンドが使えるようになります。

$ npm i -g tplink-lightbulb
$ tplight
Usage: tplight <COMMAND>

コマンド:
  tplight scan                              Scan for lightbulbs
  tplight on <ip>                           Turn on lightbulb
  tplight off <ip>                          Turn off lightbulb
  tplight temp <ip> <color>                 Set the color-temperature of the
                                            lightbulb (for those that support
                                            it)
  tplight hex <ip> <color>                  Set color of lightbulb using hex
                                            color (for those that support it)
  tplight hsb <ip> <hue> <saturation>       Set color of lightbulb using HSB
  <brightness>                              color (for those that support it)
  tplight cloud <ip>                        Get cloud info
  tplight raw <ip> <json>                   Send a raw JSON command
  tplight details <ip>                      Get details about the device

オプション:
  -h, --helpヘルプを表示                                                [真偽]
  --versionバージョンを表示                                            [真偽]

例:
  tplight scan -h     Get more detailed help with `scan`command
  tplight on -h       Get more detailed help with `on`command
  tplight off -h      Get more detailed help with `off`command
  tplight temp -h     Get more detailed help with `temp`command
  tplight hex -h      Get more detailed help with `hex`command
  tplight hsb -h      Get more detailed help with `hsb`command
  tplight cloud -h    Get more detailed help with `cloud`command
  tplight raw -h      Get more detailed help with `raw`command
  tplight details -h  Get more detailed help with `details`command

You need a command.

シェルスクリプトを書く

tplightコマンドを使ったシェルスクリプトを用意します。
スクリプト内のライトのIPアドレスは環境に合わせて書き換えてください。
~/bin/lightなどパスを通しているディレクトリにスクリプトを置きます。

#!/bin/bashLIGHT_IP=192.168.1.14

ON_OFF=1
COLOR_TEMP=2500
BRIGHTNESS=100

if["${1}"="on"];then
  ON_OFF=1
elif["${1}"="off"];then
  ON_OFF=0
elif["${1}"="hot"];then
  ON_OFF=1
  COLOR_TEMP=2500
elif["${1}"="cold"];then
  ON_OFF=1
  COLOR_TEMP=9000
fi

if[-n"${2}"];then
  BRIGHTNESS=${2}fi

tplight raw ${LIGHT_IP}\"{
  \"smartlife.iot.smartbulb.lightingservice\": {
    \"transition_light_state\": {
      \"on_off\": ${ON_OFF},
      \"color_temp\": ${COLOR_TEMP},
      \"brightness\": ${BRIGHTNESS},
      \"hue\": 0,
      \"saturation\": 0
    }
  }
}"> /dev/null

実行する

ライトを点ける

$ light on

ライトを消す

$ light off

暖色系のライトを点ける

$ light hot

寒色系のライトを点ける

$ light cold

暖色系のライトで輝度を20にする(max 100)

$ light hot 20

カスタマイズする

$ tplight details [IP address]コマンドで現在のライトの状態・設定値をみることができます。
設定値のlight_stateあたりを眺めて $ tplight raw [json]でjsonを投げてやれば輝度や色温度を変えることができます。
on_offは1でon、0でoff、brightnessは輝度、color_tempは色温度、hue, saturationはカラー対応(KL130)の場合に色味の設定ができます。

$ tplight details 192.168.1.14
{
  "sw_ver": "1.8.11 Build 191113 Rel.105336",
  "hw_ver": "1.0",
  "model": "KL130(JP)",
  "description": "Smart Wi-Fi LED Bulb with Color Changing",
  "alias": "アプリで設定した名前がここに設定される",
  "mic_type": "IOT.SMARTBULB",
  "dev_state": "normal",
  "mic_mac": "xxxxxxxxxxxx",
  "deviceId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "oemId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "hwId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "is_factory": false,
  "disco_ver": "1.0",
  "ctrl_protocols": {
    "name": "Linkie",
    "version": "1.0"
  },
  "light_state": {
    "on_off": 1,
    "mode": "normal",
    "hue": 0,
    "saturation": 0,
    "color_temp": 2500,
    "brightness": 100
  },
  "is_dimmable": 1,
  "is_color": 1,
  "is_variable_color_temp": 1,
  "preferred_state": [
    {
      "index": 0,
      "hue": 0,
      "saturation": 0,
      "color_temp": 2700,
      "brightness": 50
    },
    {
      "index": 1,
      "hue": 0,
      "saturation": 75,
      "color_temp": 0,
      "brightness": 100
    },
    {
      "index": 2,
      "hue": 120,
      "saturation": 75,
      "color_temp": 0,
      "brightness": 100
    },
    {
      "index": 3,
      "hue": 240,
      "saturation": 75,
      "color_temp": 0,
      "brightness": 100
    }
  ],
  "rssi": -69,
  "active_mode": "none",
  "heapsize": 284368,
  "tid": "tty.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "err_code": 0,
  "lamp_beam_angle": 150,
  "min_voltage": 110,
  "max_voltage": 120,
  "wattage": 10,
  "incandescent_equivalent": 60,
  "max_lumens": 800,
  "color_rendering_index": 80
}

リンク

KL110 : https://www.tp-link.com/jp/home-networking/smart-bulb/kl110/
KL130(マルチカラー対応) : https://www.tp-link.com/jp/home-networking/smart-bulb/kl130/

tplink-lightbulb : https://www.npmjs.com/package/tplink-lightbulb


Expressコマンドがありません(Windows)

$
0
0

一週間悩んだ・・・

bash: express: command not found

ググり、そして出版会社にもメールを投げた

パスが通ってないっていうのがかなり記事としてあがっていたが、どうも違う。
出版会社に問い合わせたらnpm install -g express-generator をsudoで実行するというのもあったが、
そもそもsudoの時点でコマンドがありません。お手上げ。

もしかしてnode.jsのバージョンが古いのでは?

一旦アンインストールし、最新版をインストールしてからnpm install -g express-generator

そして・・・expressコマンド実行

$ express -e ex-gen-app

コマンドが通った!

$ express -e ex-gen-app

 warning: option `--ejs' has been renamed to `--view=ejs'


   create : ex-gen-app\
   create : ex-gen-app\public\
   create : ex-gen-app\public\javascripts\
   create : ex-gen-app\public\images\
   create : ex-gen-app\public\stylesheets\
   create : ex-gen-app\public\stylesheets\style.css
   create : ex-gen-app\routes\
   create : ex-gen-app\routes\index.js
   create : ex-gen-app\routes\users.js
   create : ex-gen-app\views\
   create : ex-gen-app\views\error.ejs
   create : ex-gen-app\views\index.ejs
   create : ex-gen-app\app.js
   create : ex-gen-app\package.json
   create : ex-gen-app\bin\
   create : ex-gen-app\bin\www

   change directory:
     $ cd ex-gen-app

   install dependencies:
     $ npm install

   run the app:
     $ DEBUG=ex-gen-app:* npm start


最後に

よくよく考えたら確証もってバージョンが原因とも言い切れないのでは?と思ったが
何度も最初からやり直したので今さら手順が間違っていたとは思えない。
次は解決するだけでなく原因をもっと明確にしよう。

Node.jsでBASIC認証.

$
0
0

Node.jsでBASIC認証

Node.jsでexpressを使ったWebサーバでBASIC認証を導入する場合の実装方法について紹介します.
本記事上では,パスワードを暗号化・ハッシュ化せずにソースコード上に記載しております.セキュリティ上望ましくない点にご注意ください.

BASIC認証の導入

expressでBASIC認証を導入するにはexpress-basic-authを使用します.下記がそのサンプルコードです.

constexpress=require("express")constbasicAuth=require("express-basic-auth")// 正解のユーザ名とパスワードconstcorrectUserName="hiroyky"cosntcorrectPassword="password"constapp=express();app.use(basicAuth({challenge:true,unauthorizedResponse:()=>{return"Unauthorized"// 認証失敗時に表示するメッセージ},authorizer(username,password)=>{constuserMatch=basicAuth.safeCompare(username,correctUserName)constpassMatch=basicAuth.safeCompare(password,correctPassword)returnuserMatch&passMatch}}))

basicAuthをexpressのミドルウェアとして登録しています.引数のオブジェクト中のauthorizerに認証用関数を書きます.戻り値がtrueの場合,認証OK,falseなら否認です.

このとき注意したいのが公式ドキュメントにもあるように下記の記載です.内容としては以下の実装を推奨しています.素直に従っておきましょう.

  • 比較に=====といった演算子を用いない,safeComapre関数によって比較する
  • return userMatch & passMatchのように論理演算によって値を出力する

When using your own authorizer, make sure not to use standard string comparison (== / ===) when comparing user input with secret credentials, as that would make you vulnerable against timing attacks. Use the provided safeCompare function instead - always provide the user input as its first argument. Also make sure to use bitwise logic operators (| and &) instead of the standard ones (|| and &&) for the same reason, as the standard ones use shortcuts.

これで,ひとまずBASIC認証を導入することができました.

obnizと圧電スピーカーを使ってキーボードをキーボード(音楽)に変えてみた

$
0
0

はじめに

いまOTTOというArduinoベースのオープンソースロボットをobnizに移植する個人プロジェクトを進めています(IoTLT vol.54LTしたやつです)。
OTTOには圧電スピーカーも搭載されており、これを使ってロボっぽいピロピロ音を出すことができます。

先日、この圧電スピーカーを使ってobniz版OTTOに今流行りのパプリカを歌わせてみました。

圧電スピーカーで音を鳴らすのは周波数を渡すだけととても単純だったので、これを応用してobnizと圧電スピーカーを使って普段コードを打ち込んでるキーボードをキーボード(音楽用)に変えてみたいと思います。

ハードウェア実装

まずは圧電スピーカーを用意します。
obnizのパーツライブラリページに秋月やSwitch Scienceの通販リンクがあります。
大きさとかで30円~100円と割と種類ありますが、なんでもいいと思います。
私は手元にあったやつを使いました。

IMG_20191204_225038.jpg

このうち左下のやつは飛び抜けて音がでかかったため除外しました。
他のは大体同じ音量ですが、左上のは音が悪かったです。

そしてこれらをobnizに直挿しします。
ブレッドボードを使えば12個繋げられますが、obnizと言えばモーター然り直差しなので、このままいきます。

IMG_20191204_221332.jpg

ソフトウェア実装

キーボードのイベント拾うならHTMLで実装するのが手軽ですが、画面上にキーボードの画像表示してどこが押されてるかとかの描画もないといけない感じがするので、HTMLは使いません。
代わりにNode.jsで実装します。

Node.jsでリアルタイムにキーイベントを拾うには↓のパッケージが良さそうです。
iohook

こんな感じでキーイベントだけでなくマウスポインターとかも拾えます。

index.js
'use strict';constioHook=require('iohook');ioHook.on("mousemove",event=>{console.log(event);// result: {type: 'mousemove',x: 700,y: 400}});ioHook.on("keypress",event=>{console.log(event);// result: {keychar: 'f', keycode: 19, rawcode: 15, type: 'keypress'}});//Register and stark hook ioHook.start();

あとは圧電スピーカーの制御コードをobnizのパーツライブラリからコピって、keydownで音鳴らしてkeyupで音止めるようにすればいいわけですね。

完成したコード

こちらが完成したコードです。
キーボードのキーコードとスピーカーで鳴らす周波数のマップオブジェクトを用意しています。
それと各圧電スピーカーの割当の制御とかも書いてます。
あとは上記で書いたような実装です。

npm install obniz iohookしたうえで、obniz IDと各スピーカーのピン番号を書き換えて実行してみてください。

index.js
"use strict"constioHook=require("iohook")constObniz=require("obniz")constkeymap={"16":370,// F#3"30":392,// G3"17":415,// G#3"31":440,// A4"18":466,// A#4"32":494,// B4"33":523,// C4"20":554,// C#4"34":587,// D4"21":622,// D#4"35":659,// E4"36":698,// F4"23":740,// F#4"37":784,// G4"24":831,// G#4"38":880,// A5"25":932,// A#5"13":988,// B5"39":1047,// C5"41":1109,// C#5"27":1175,// D5"26":1245,// D#5}constobniz=newObniz("xxxx-xxxx")obniz.onconnect=async()=>{constspeakers=[]speakers.push(// スピーカーは繋げられるだけここに列挙{assign:0,obniz:obniz.wired("Speaker",{signal:0,gnd:1})},{assign:0,obniz:obniz.wired("Speaker",{signal:2,gnd:3})},{assign:0,obniz:obniz.wired("Speaker",{signal:4,gnd:7})},)ioHook.on("keydown",event=>{if(!keymap[event.keycode])returnif(speakers.some(speaker=>speaker.assign===event.keycode))returnfor(constspeakerofspeakers){if(speaker.assign)continuespeaker.assign=event.keycodespeaker.obniz.play(keymap[event.keycode])return}})ioHook.on("keyup",event=>{if(!keymap[event.keycode])returnfor(constspeakerofspeakers){if(speaker.assign!==event.keycode)continuespeaker.assign=0speaker.obniz.stop()return}})ioHook.start()}

キー割り当て

こんな感じで音程を割り当てています。

assign.png

演奏してる様子

ひとまずドレミファソラシド。

スピーカー3つ繋いでるので3和音まで出せます。
(直差しじゃなければ12和音まで出せます)

最後に一曲演奏してみます。
なんの曲かわかったらまじ神です。

ちなみに所々音出てないのは回路やパーツやプログラムが悪いわけではなく、多分指を離すのが遅くて3和音鳴ってる状態のまま次のキー押しちゃってるのが問題です。
つまり演奏が下手なだけです。

おわりに

ここまで書いてそもそもこれはIoTなのか疑問になりましたが、obnizはネットにつながってないと動かないのでこれはきっとIoT。

はじめてのGitHubActionsMarketplace公開 - PRにマイルストーンをつけるActionをつくってみた

$
0
0

Actions使ってなにかするworkflowをつくるんじゃなくてAction自体をつくる話。

なにか簡単にGitHubActionsをつくってみたいと思い、GitHub公式のactions/labelerを参考にPRにラベルじゃなくてマイルストーンをつけるActionをつくろうと考えた。
Marketplaceで検索した結果、labelの方はちょいちょいあったがmilestoneの方はそんなにはまだなかった。マイルストーンの作成や更新がしたいのではなくてPRに既存のマイルストーンを結びつけたいってなると1,2件ぐらいしか無さそう。それらもこちらのやりたいこととは違った。
最終的につくったのはiyu/actions-milestone

GitHub公式のテンプレートからリポジトリを作成する

GitHubのテンプレート機能は実は使ったことがなかったが、用意してくれているのだから使わない理由はない。javascript版(actions/javascript-action)とtypescript版(actions/typescript-action)が用意されている。あとはテンプレートと呼べるのか謎のミニマムなコンテナ版(actions/container-action)もあった。今回は無難にtypescriptを選択する。

image.png
[Use this template]って書いてある緑色のボタンからリポジトリを作成する。

image.png
こんな画面が出てくるがあとはいつものリポジトリ作成と同じ手順。

image.png
作成し終わるとforkしたリポジトリみたいにリポジトリ名の下にテンプレートに使ったリポジトリが表示されていた。ただコミットとかは引き継がれずすべてコピペしてInitial commitだけしたような感じの状態だった。別の世界線というより別世界だからテンプレートの今後のコミットをcherry-pickするのは難しいかもしれない。そんな心配する必要もないが。

テンプレートでできたリポジトリからconfig情報を書き換える

すぐにプログラミングに取り掛かりたいところだけどまずはconfigだけを書き換える。完全に好みの問題だがプログラムの修正も全部同じコミットにすると次回テンプレートから作成したときに何が必要になるのかわかりにくいなと思ったので。
やることは、

  • README.mdの書き換え
    • Actionの開発方法とかが書いてあるので全部消して書き換えでいい
  • package.json, package-lock.jsonの書き換え
    • リポジトリ名だったり説明文だったりurlだったり書き換える
    • package-lock.jsonの方はpackage.json書き換えた後にnpm installでもしとけば自動で書き換わるので手動で書き換える必要はない
  • action.ymlの書き換え
    • action名だったり説明文だったり
    • この段階である程度input,output決まってるなら書き換えてもいいような、まだなような?

こちら書き換えたときのコミットなので参考に
https://github.com/iyu/actions-milestone/commit/4101ca509ee0fc10f84542abd65e416a3feca566

プログラムを書く

ここからやっとプログラミング

ESLintの設定

汚いコードをあとから修正するのは面倒なので最初に入れる。
tslintは2019年内に開発終了するらしいのでtypescriptのlintにはESLintを使う。

$ npm install--save-dev eslint

初期セットアップ (対話型のCLIでなんか色々聞かれるけど詳細は割愛)
image.png

ものすごくただの好みの問題だが自分はeslint-config-airbnb-baseを長らく使っている。

$ npm install--save-dev eslint-config-airbnb-base

このテンプレートではtestにjestを使っているみたいなのでjestのプラグインも入れる。

$ npm install--save-dev eslint-plugin-jest

最終的な.eslintrc.ymlはこんな感じ。好みで変えていく。

env:es6:truenode:truejest/globals:trueextends:-airbnb-baseglobals:Atomics:readonlySharedArrayBuffer:readonlyparser:'@typescript-eslint/parser'parserOptions:ecmaVersion:2018sourceType:moduleplugins:-'@typescript-eslint'-jestrules:no-console:offno-unused-vars:off'@typescript-eslint/no-unused-vars':errorsettings:import/resolver:node:extensions:-.ts

実行時にtypescriptの拡張子を教えてあげる必要がある。早く省略できるようにして欲しい。

$ eslint --ext .ts,.js .

今回作るもの

大まかに区分するとこんな感じ。

  1. inputデータを取得
  2. PRの情報を取得
  3. configファイルの中身を取得
  4. ブランチ名からマイルストーンを割り出す
  5. PRにマイルストーンをつける

inputデータを取得する

action.ymlに欲しいinputデータを記述しておく。

name:'PullRequestMilestone'description:'AddmilestonetoPRs'author:'iyu'inputs:repo-token:description:'TheGITHUB_TOKENsecret'configuration-path:description:'Thepathforthemilestoneconfigurations'default:'.github/milestone.yml'outputs:milestone:description:'TheAddedMilestone'runs:using:'node12'main:'lib/main.js'

上記の場合だとrepo-tokenconfiguration-path
これをAction利用者が入力してくれるはずなのでそれをtypescript側で取得する。
こういう基本的なものはすべて公式のライブラリが取得方法を用意してくれているので簡単に取得できる。

import*ascorefrom'@actions/core';consttoken=core.getInput('repo-token',{required:true});constconfigPath=core.getInput('configuration-path',{required:true});

requiredつけておけば値が存在しなかった時のエラー処理もいい感じにやってくれるのでエラー文考えなくても良い。便利。
(action.ymlでdefault付いてるくせにrequiredにしているのはただの嘘つき)

PRの情報を取得する

どのイベントから発火されたのかも公式のライブラリで簡単に取得できる。

import*asgithubfrom'@actions/github';constpullRequest=github.context.payload.pull_request;if(!pullRequest){console.log('Could not get pull_request from context, exiting');return;}

ブランチ名を利用してマイルストーンをつけるActionなのでPR以外のイベントの場合は上記のようになんかメッセージ表示して終了させる。

後々にPRのBaseブランチやHeadブランチの名前や現在のマイルストーンを利用したいのだが、このライブラリのtypesがしっかり全部書いてないので必要なパラメータは自分で拡張する必要がある。かなしい。

import*asgithubfrom'@actions/github';import{WebhookPayload}from'@actions/github/lib/interfaces';interfacePullRequestWebhookPayloadextendsWebhookPayload{pull_request?:{[key:string]:any;number:number;html_url?:string;body?:string;milestone?:string;base:{ref:string;},head:{ref:string;},},}constpullRequest=(github.context.payloadasPullRequestWebhookPayload).pull_request;

configファイルの中身を取得

inputで指定されたconfigファイルパスを取得する。action利用者がcheckoutをしているとも限らないのでファイルはAPI経由で取得する。このへんはactions/labelerを参考にしている。

import*asgithubfrom'@actions/github';(async()=>{// さっきのやつ// const token = core.getInput('repo-token', { required: true });// const configPath = core.getInput('configuration-path', { required: true });constclient=newgithub.GitHub(token);constresponse=awaitclient.repos.getContents({owner:github.context.repo.owner,repo:github.context.repo.repo,path:configPath,ref:github.context.sha,});consttext=Buffer.from((response.dataas{content:string}).content,'base64').toString();})();

(APIクライアントはPromise対応しているのでasync関数で囲ってる)
これでAPI経由でファイル取得可能だ。レスポンスの中のファイル内容についてはbase64文字列なのでtext変換している。

configファイルはこんな形式のyamlファイルを想定している。

base-branch:-"(master)"-"releases\\/(v\\d+)"head-branch:-"feature\\/(v\\d+)\\/.+"

yamlファイルの文字列持っててもtypescriptで扱いにくいので今回はこんな感じのパーサーを用意して変換した。

import*asyamlfrom'js-yaml';interfaceConfigObject{baseBranchList:RegExp[];headBranchList:RegExp[];}constparse=(text:string)=>{constconfig:{'base-branch'?:string[],'head-branch'?:string[],}=yaml.safeLoad(text)||{};constresult:ConfigObject={baseBranchList:[],headBranchList:[],};result.baseBranchList=(config['base-branch']||[]).map((item)=>newRegExp(item));result.headBranchList=(config['head-branch']||[]).map((item)=>newRegExp(item));returnresult;};

ブランチ名からマイルストーンを割り出す

このへんはもうこのAction固有の処理なのでそんなに詳しく書きません。baseブランチ、headブランチはWebhookのpayloadの中に入っているのでそれを使う。

constpullRequest=(github.context.payloadasPullRequestWebhookPayload).pull_request;const{milestone,number:prNumber,base:{ref:baseBranch},head:{ref:headBranch},}=pullRequest;

先程のconfigで取ってきた正規表現とmatchするものを見つけてくる。milestoneは1つしかつけることはないので1件ヒットしたら即返却で良い。

exportconstmatch=(baseBranch:string,headBranch:string,configObject:ConfigObject,):string|undefined=>{lethit:string|undefined;configObject.baseBranchList.some((regexp)=>{constm=baseBranch.match(regexp);if(m&&m[1]){([,hit]=m);}return!!hit;});if(hit){returnhit;}configObject.headBranchList.some((regexp)=>{constm=headBranch.match(regexp);if(m&&m[1]){([,hit]=m);}return!!hit;});returnhit;};

PRにマイルストーンをつける

実はちょっとハマった。30分ぐらい悩んだ。
actions/labelerを参考にしていたので、最後にaddLabelのmilestone版のgithubのAPIを叩けば終わると思っていたがSDKにそんな関数は見当たらない。最初はpayloadが不完全だったこともあってSDKが対応していないだけだろうと思っていたのだがAPIリファレンスにもaddMilestoneだったりsetMilestoneなるものは見当たらなかった。悩んだ。
結論、milestoneはissueUpdate(PRもissueの一種なので同一API)やissueCreate時につけてやるものらしい。labelだけaddLabelなんてAPIがあるから騙された。issueのtitleを変える感覚でmilestoneも変更するみたい。

さらにいうとlabelと違ってmilestoneはmilestone用のID(下記コードのnumberの部分)を使ってissueを更新する。先程の項目で割り出したのはmilestoneのタイトルなのでそれと一致するmilestoneのIDを既存のmilestone一覧から探してこなくてはならない。

constaddMilestone='v1.0';constmilestones=awaitclient.issues.listMilestonesForRepo({owner:github.context.repo.owner,repo:github.context.repo.repo,});const{number:milestoneNumber}=milestones.data.find(({title})=>title===addMilestone)||{};

既存のmilestoneが見つからなかった場合はmilestoneCreateとかで作成しても良かったわけだが、今回はメッセージ出すだけにした。やるならオプションで作成も一緒にするかみたいなフラグを追加しようかな。

if(milestoneNumber){awaitclient.issues.update({owner:github.context.repo.owner,repo:github.context.repo.repo,issue_number:prNumber,milestone:milestoneNumber,});core.setOutput('milestone',addMilestone);}else{console.log(`Milestone not found, Please create it first "${addMilestone}".`);}

今回は紐付けたmilestoneをoutputとして出力するactionなのでcore.setOutput('milestone', addMilestone);という一文も忘れずに。

Marketplaceに公開する

公式のドキュメントに画像つきで丁寧に書いてあるのでそこまで説明いらないとは思う。
https://help.github.com/ja/actions/automating-your-workflow-with-github-actions/publishing-actions-in-github-marketplace

公開する成果物にはnode_modulesも含めビルド済みのjsファイルがないといけないので、masterブランチではgitignoreにnode_modulesが入っていてそれとは別にnode_modulesを除外しないreleaseブランチを作ろう、とテンプレートでは書かれている。
https://github.com/actions/typescript-action#publish-to-a-distribution-branch

他の手段としては@zeit/nccを使ってそもそもnode_modulesも含めすべて1ファイルにしてしまうという方法もある。
https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-a-javascript-action

今回は前者の方法でやろうと思うが毎回そんな作業をするのも面倒なのでnpm preversionにビルド作業とnode_modules,ビルド成果物の追加を仕込んでおくことにした。package.jsonscriptsの部分を書き換える

"scripts":{"preversion":"npm ci && npm run build && npm ci --production && git add node_modules lib -f"}

該当する作業コミットはこれ。
https://github.com/iyu/actions-milestone/commit/a95202bc14122a66a01dc5aad55a6ebfed5279bd

これは何なのかというと、npm versionというpackage.jsonのversion部分をアップデートしつつgit-tagをつけてくれるコマンドの前にhookされるスクリプト。npmにライブラリを公開している人ならおなじみだと思う。
詳しくはここ https://docs.npmjs.com/cli/version

$ npm version major

> actions-milestone@1.0.0 preversion iyu/actions-milestone
> npm ci && npm run build && npm ci --production&& git add node_modules lib -f

npm WARN prepare removing existing node_modules/ before installation
added 651 packages in 7.098s

> actions-milestone@0.0.0 build git/iyu/actions-milestone
> tsc

npm WARN prepare removing existing node_modules/ before installation
added 50 packages in 2.174s
v1.0.0

やってることはシンプルで、

  1. npm ci node_modulesを消してからinstall (clean installの略だった気がする)
  2. npm run build scriptsのbuildを実行 (typescriptのビルドが書かれてる)
  3. npm ci --productionビルドのときしか必要でないpackageを消すために実行してる
  4. git add node_modules lib -f .gitignoreにかかれているnode_modulesとビルド成果物をforce add

このあとに本来のnpm versionが動く。 (package.jsonのversionの更新とgit-commit, git-tag)

githubにpushしたらリリース物として[Draft a new release]のボタンを押してPublish this Action to the GitHub Marketplaceにチェックする。
image.png
カテゴリとかiconとか必要なものを埋めていく。iconは別になくてもいいっぽい。

これで無事完成!検索すればちゃんとMarketplaceに出てくるようになった!
image.png

さいごに

今回は一発目だったので簡単なActionにしたがもっとちゃんと実用性のあるActionを作っていきたい。ただ仕事で使うやつとなるとたぶんMarketplaceには上げないと思うので今回はまぁ楽しめた。
近々、チーム開発でGitHubActionsを使うにあたって苦労した点を記事にして書きたいと思う。

Heroku + LINE + node.js (Express.js)でオウム返しボットを作成する

$
0
0

Heroku + LINE + node.js (Express.js)でオウム返しボットを作成する

 
※本記事は最終的にQ&Aチャットボットを構築するための一部分となります。
本編はこちら

何故、オウム返しなのか?

オウム返しはチャットボット構築の基礎中の基礎になります。
オウム返しすることで以下の3点が確認・理解できます。

①単純な疎通確認
 ・今回だと、スマホ ←→ LINE ←→ メッセージングAPI ←→ Herokuで稼働するアプリまでの疎通確認になります

②単純な動作確認
 ・LINEで発話した内容を正常に受信できることの確認
 ・アプリケーションから発話した内容がLINEで正常に受信できることの確認

③疎通に必要な手順の理解、メッセージのやりとりに必要な手順の理解

はじめて使う技術でWebアプリケーション作成するときに最初にやるhello worldと同じですね。
単純なことですがこれらを最初にやるかやらないかでは技術の習得レベルに差が出ると思います。

 
 

事前準備

Visual Studio Code(以下、VSCode)をインストールします

▼インストールはこちら
https://azure.microsoft.com/ja-jp/products/visual-studio-code/

何故、VScodeなのか?
現状、最強の統合開発環境だと思っています。
長くeclipseの時代が続きましたが、以下を理由に最近はVSCodeしか使っていないです。
『とにかく軽い』、『しょぼいスペックのPCでも快適に使える』、『バグや癖がなく安定している』

image.png

Herokuのアカウントを作成しておきましょう

▼Herokuはこちら
https://jp.heroku.com/free

何故、Herokuなのか?
①クレジットカードの登録が不要
無料のアプリケーションプラットフォームサービスは沢山ありますが
クレジットカードの登録が不要で、制約が最も緩いのがHerokuだと思います。
ちょっとした技術検証や学習に向いたアプリケーションプラットフォームだと思います。

②情報が豊富
間口の広さがあってか、Herokuの利用に際して困ったことがあれば
情報が豊富なので、Google先生に聞けばほぼ解決できます。

image.png

LINE BusinessIDに登録してメッセージングAPIの準備をしましょう

▼LINE BusinessIDはこちら
https://account.line.biz/login

↓↓↓ メッセージングAPI開始までの手順を別の記事で公開してます。

▼LINEメッセージングAPIでLINEのロボットアカウントを作成する手順
https://qiita.com/abemaki/items/76e240828aee92fbcff7

ソースコードを格納するための作業フォルダを作成しておきましょう

image.png

herokuで新規アプリを作成

1.新規アプリ作成

image.png

2.名前だけ決めてアプリ作成

image.png

3.herokuの手順に従って、Gitアプリと接続します

image.png

herokuの手順に従い事前準備を行います。
コマンドプロンプトを使用します。

heroku login
cd /d D:\heroku\qandabot/ ← 事前に作成しておいたフォルダに移動します
git init
heroku git:remote -a qandabotdemo

 
 
 

ここまで順調だとおおおそ以下のような表示になります。
image.png

アプリケーションの土台を作成します

1.npm init と install で土台作り

ベースとなるアプリを作成して、必要なライブラリをアプリいっきにインストールします。

npm init --y
npm install dotenv express xlsx elasticlunr cfenv request body-parser --save

 
各ライブラリの用途は以下にまとめてあります。

リソース説明
dotenv環境設定ファイルの読み込みに活用
expressWebサーバとして活用
xlsxエクセルの読み込みに活用
elasticlunrJSの全文検索エンジンとして活用
body-parserリクエストの解釈を簡易化するために活用
requestLINEに応答リクエストを返すために活用
cfenv環境情報の取得を簡易化するために活用

 
 

2.作成したリソースベースにVSCode上で開発を進めます

VSCodeでリソース(作業用フォルダ)を開きます

image.png

 
 

.env(環境設定ファイル)ファイルを作成します

LINEメッセージングAPIの設定情報を.envファイルに記述します
最終的にLineメッセージングAPI と LINEシュミレータを切り替えながら開発することになるので
環境設定情報は環境設定ファイルに書いておくと後々とても便利です。

記述用テンプレート
事前準備で用意したLineメッセージングAPIの設定情報を.envファイルに書いていきます

# LINEメッセージングAPIの設定情報
Channel_secret=<チャネルシークレット>
Channel_user_id=<チャネルユーザーID>
Channel_access_token=<チャネルアクセストークン>
Channel_endpoint=https://api.line.me/v2/bot/message/reply

 
 
 

index.jsファイルを作成します

下記を参考(コピペ)にindex.jsファイル内に
オウム返しの為の処理を記述していきます。

// ******************************************************************//// ** 初期設定関連 ここから                                          **//// ******************************************************************//varexpress=require("express");varapp=express();varcfenv=require("cfenv");require('dotenv').config();varrequest=require("request");varbodyParser=require("body-parser");app.use(bodyParser.urlencoded({extended:true}));app.use(bodyParser.json());// ******************************************************************//// ** 設定情報の取得 ここから                                        **//// ******************************************************************//// LINEの設定varurlLine=process.env.Channel_endpoint// BOTの標準キーvarbotKey=process.env.Channel_access_token// ******************************************************************//// ** メッセージ処理 ここから                                      **//// ******************************************************************//constasyncwrap=fn=>(req,res,next)=>fn(req,res,next).catch(next);app.post("/api",asyncwrap(async(req,res)=>{// 受信テキストvaruserMessage=req.body["events"][0]["message"]["text"];// とりあえずオウム返しoptions=createAnsMessage(req,userMessage+"って言いましたね?");// メッセージを返すrequest(options,function(err,res,body){});res.send("OK");}));// ******************************************************************//// **   ファンクション処理 ここから                                  **//// ******************************************************************//// ---- ************************ -------// ---- 一答形式の回答を作成する   -------// ---- ************************ -------functioncreateAnsMessage(req,message){varoptions={method:"POST",uri:urlLine,body:{replyToken:req.body.events[0].replyToken,messageNotified:0,messages:[// 基本情報{contentType:1,type:"text",text:message,}]},auth:{bearer:botKey},json:true};returnoptions;}// サーバ起動varappEnv=cfenv.getAppEnv();app.listen(appEnv.port,"0.0.0.0",function(){console.log("server starting on "+appEnv.url);});

 
 

package.jsonのscripts要素の中を修正します

これによりnpm startコマンドでアプリケーションが起動できるようになります。
node index.jsでも起動できますが、起動ファイルがシステムによって異なるのでどのシステムでも
npm startで起動できようにしておくと便利です。

"scripts":{"start":"node index.js","test":"echo \"Error: no test specified\"&& exit 1"},

3.動作確認の為のここでherokuにコミット&プッシュします

herokuの公式手順に従います

git add .
git commit -am "動作確認の為のオウム返し"
git push heroku master

 
 

4.herokuのエンドポイントをLINEメッセンジャーAPIに設定する

エンドポイントをherokuで確認
今回作成するアプリだと最終的なエンドポイントは『エンドポイント/api』になります
image.png
 

 
エンドポイントをLINEメッセンジャー側のWebhookに設定します 
image.png

 

Webhookを設定したら、検証して問題ないことを確認し
Webhookを有効化します
image.png

 

5.LINEからBOTに話しかけて、オウム返しが動作することを確認します

QRコードから友達登録して話しかけてみてテストします

image.png

 

これで疎通やメッセージのやりとりに必要な作業は一通り完了です。

Viewing all 8820 articles
Browse latest View live