- 投稿日:2019-12-15T23:19:39+09:00
SlackのWebhookをプロキシする仕組みを作る
Slackはさまざまなカスタマイズ機能を持っているのが魅力のツールです。例えばBotを作ったり、カスタムのslash commandを作ったりすることで、プラットフォームの拡張ができます。
Slack Botの作り方はいくつかあるのですが、Slackのリッチな機能を最大限に引き出すには、SlackからのWebhookを受けることが必要になってきます。すなわち、ボタンなどが付いたリッチなメッセージの投稿は難しくないのですが、投稿したメッセージのボタンやメニュー操作は、SlackからWebhookの形で通知される仕組みになっています。
※ この辺りの仕組みの詳細については、まとまっている記事がいくつもあるので省略します。
Slackは当然publicなサービスなので、インターネット経由でWebhookが飛んできます。これを受けて処理するためには、Bot用の公開APIサーバを書くことになるでしょう。
公開サーバ? 何か問題でも?
それじゃあ、単にサーバを立てればいいじゃないかというのはもっともですが、サーバを公開するのはいくつか面倒な点があります。
- 公開サーバはセキュリティリスクがある
- もちろん、ちゃんと管理すればいいのですが、publicに露出するものが増えると相応にリスクが増加してしまうのはどうしようもないことです。できればやりたくありません
- とりわけ会社業務で使う場合、イントラネットにある社内システムや社内ネットワークとの接続性を考慮する必要が出てくる
- 当然、Botであるからには、社内のいろいろなものと連携させたくなります。
SlackのRTM APIというWebSocketベースのAPIでchatメッセージを受信する場合には、こういった悩みは発生しませんでした(メッセージをpullするAPIを使うので、基本的にインターネットへ接続できるBot実行環境がありさえすればよかった)。できる限りそのお手軽さを保ったまま、セキュリティなどの煩雑な問題をコントロールする方法がないか、ということを考えたくなります。
そうだプロキシしよう
というわけで、次のようなシステムを考えることにしました。
- Webhookを直接Botサーバに流すのではなく、プロキシ(リバースプロキシ)を経由させる
- プロキシは単にリクエストを横流しするのではなく、リクエストの検証を行い、不正なリクエストを弾く
- Slackから事前に払い出された秘密鍵を元に、指定の手順でHMACを計算することで、リクエストの正当性の検証が可能です
- Verifying requests from Slack
- SDKがあるので、Slack社オフィシャルの実装をそのまま引っこ抜いてくることができます
具体的には、AWSのAPI GatewayとLambdaを組み合わせた、簡単なプロキシを書きます。
パターンとして変則的ではありますが、結果的にAWSのDDoS対策ホワイトペーパーで述べられている、API Gatewayに公開箇所を絞るというプラクティスに沿っているように思います。
Typically, when you must expose an API to the public, there is a risk that the API frontend could be targeted by a DDoS attack. To help reduce the risk, you can use Amazon API Gateway as a “front door” to applications running on Amazon EC2, AWS Lambda, or elsewhere.
By using Amazon API Gateway, you don’t need your own servers for the API frontend and you can obfuscate other components of your application. By making it harder to detect your application’s components, you can help prevent those AWS resources from being targeted by a DDoS attack.実装例
今回は(ちょっと慣れない言語なのですが)Node.jsでlambdaを書いてみることにします。
フレームワークとしてserverless frameworkを使うとお手軽にAPI Gateway + Lambdaの構成を作ることができました。https://github.com/saka1/slack-webhook-gatekeeper
バックエンドのSlack App名に対応したURLパスに対してWebhookが飛んでくると、秘密鍵を使ってWebhookの検証をします。あらかじめ、Slack App名と秘密鍵の対応関係はparameter storeに入れておきます。正当なWebhookだった場合にはupstreamにリクエストを飛ばしてプロキシ動作をします。URLパスは複数登録することができるように作りました。
学んだこと
- AWSのparameter storeはあんまり高速ではなさそう(getに数百msかかる?)だったので、lambdaでキャッシュしたほうが無難そうでした
- プロキシとBotは1:Nの関係にあるため、プロキシシステムは全てのBot Appの秘密情報を管理しないといけなくなります(そうしなければリクエストの検証ができません)
- 理想的ではないので、何かうまい回避方法があればなあと思っています
API Gatewayからlambdaを呼ぶには「lambda統合」「lambdaプロキシ統合」の2つがあるらしいのですが、前者を使ったほうがよさそうでした。というか飛んでくるWebhookの検証でHMAC計算が入るため、1バイトでもゴミが入ると正しく動きません。そもそも前者は用途に対して複雑すぎました。
まとめ
この記事では、Slack Botの前段に置くプロキシシステムを検討し、その実装までをやってみました。ざっくり机上で検討 → ざっと実装した割には案外使えそうなものが考案できて、個人的には満足しています。
- 投稿日:2019-12-15T19:59:14+09:00
Next.jsでcookieをシンプルに扱うことができるライブラリ nookies を紹介
Next.jsでcookieを扱うのは大変
Next.jsなどのサーバーサイドレンダリング(以下SSR)をしているフレームワークでcookieを扱うのは面倒くさいですよね。
その理由の一つとして、同じコードでもSSRの場合とクライアントでレンダリングしている場合で挙動が違うということがあります。
例をお見せしましょうクライアントでレンダリングしている場合
console.log(document.cookie); // accessToken=test1234;
SSRの場合
console.log(document.cookie); // ReferenceError: document is not defined
原因
クライアントサイド(ブラウザ)でレンダリングしている時は、ブラウザに保存されているcookieにアクセスできるが,
SSRの時はブラウザに保存されているcookieにアクセスできません。SSRの時にcookieを扱うには
SSRでcookieの情報はここに入っています
index.tsxconst TestPage: NextPage<Props> = (props) => { return <div>test</div> } TestPage.getInitialProps(ctx) { // ここ console.log(ctx.req.headers.cookie) // accessToken=test1234; return {}; }同じライブラリをクライアントとSSRで共有していたりすると、条件分岐などが大変ですね
そんな時に nookies を使います
https://www.npmjs.com/package/nookies使い方
以下の例で示すようにクライアントサイドの場合ctxを渡さずに、SSRならctxを渡せば、cookieをオブジェクトに整形して返してくれます。
tool.tsimport { parseCookies } from 'nookies'; import { NextPageContext } from 'next'; export function printCookie(ctx?: NextPageContext) { const cookie = parseCookies(ctx); console.log(cookie) // { accessToken: 'test1234' } }また、cookieの追加もクライアントとSSR分け隔てなく行ってくれます
set_cookie.tsimport { setCookie, destoroyCookie } from 'nookies'; import { NextPageContext } from 'next'; export function setCookie(ctx?: NextPageContext, token: string) { setCookie(ctx, 'accessToken', token, { maxAge: 30 * 24 * 60 * 60, }); } // ついでにcookie削除(動作確認してません) export function destoroyCookie(ctx?: NextPageContext) { destroyCookie(ctx, 'accesstToken') }ライブラリを読んでみた(箇条書きです!)
https://github.com/maticzav/nookies
nookies/src/index.tsconst isBrowser = () => typeof window !== 'undefined' // 今の環境がSSRかクライアントサイドレンダリングか調べてるらしいです . . if (ctx && ctx.req && ctx.req.headers && ctx.req.headers.cookie) { return cookie.parse(ctx.req.headers.cookie as string, options) // SSRだったらctx.req.headers.cookieに入っているcookieをparseして返却 } . . if (isBrowser()) { return cookie.parse(document.cookie, options) //クライアントだったらdocument.cookieにあるcookieをparseして返却 } . . ctx.res.setHeader('Set-Cookie', cookiesToSet) // SSRならレスポンスヘッダーにcookieをセットする . . if (isBrowser()) { | document.cookie = cookie.serialize(name, value, options) // クライアントならクッキーをセット }まとめ
以上です。いかがでしたでしょうか?
参考になりましたら幸いです。
- 投稿日:2019-12-15T19:44:52+09:00
create-react-appで作ったアプリがhttpsだと動かない
問題点
create-react-appで作成したアプリケーションにhttpsでアクセスすると、以下のようにエラーとなりました。
SecurityError: Failed to construct 'WebSocket': An insecure WebSocket connection may not be initiated from a page loaded over HTTPS.
httpだと問題なく動きます。
create-react-appで作ったアプリを試しにHerokuに上げてみたときに、この問題を踏みました。原因
以下でIssuesが上がっていました。
https://github.com/facebook/create-react-app/issues/8075
https://github.com/facebook/create-react-app/pull/8079WebSocketsを利用している箇所で、httpsの場合はwss(WebSockets over SSL)を利用しなくてはいけないところ、wsを利用してしまっているためのようです。
解決法
問題が発生しているときのpackage.jsonの依存関係は以下です。
package.json"dependencies": { "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", "react": "^16.12.0", "react-dom": "^16.12.0", "react-scripts": "3.3.0" }react-scriptの3.3.0で発生している問題なので、3.2.0にバージョンダウンすると、一旦動作するようになります。
$ npm install react-scripts@3.2.0react-scriptsのバージョン変更がpackage.jsonにも反映され、httpsでも動作するようになりました。
package.json(更新後)"dependencies": { "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.4.0", "@testing-library/user-event": "^7.1.2", "react": "^16.12.0", "react-dom": "^16.12.0", "react-scripts": "3.2.0" }上記Issuesは、react-scriptの3.3.1で修正予定(2019/12/15時点)のようなので、バージョン下げは暫定対応とし、修正されたら3.3.1に上げるのがよいと思います。
- 投稿日:2019-12-15T19:37:54+09:00
TwitterでTILしたらGitHubに草が生える
はじめに
TIL(Today I Learned): 今日学んだこと
をtwitterのつぶやきで行おうと思いました
#til
というハッシュタグで学びをつぶやく- 1日1回自分の投稿の
#til
ハッシュタグを拾いに行く- GitHubにコミット
というのがいけないかな〜と思ったのがきっかけです
AWSで
CloudWatch Events
+Lambda Functions
あたりでできそうな気がしたのでやってみます参考
準備
- AWSアカウント
- Twitter開発者アカウント
- TILをコミットしていくGitHubリポジトリ
Twitter APIを利用するためには開発者アカウントの登録が必要です
※ 利用目的とか審査とかあってやや面倒です…
手順
- Amazon CloudWatch Eventsで定期的にAWS Lambdaを起動
- LambdaでTwitter APIを叩いて
#til
ツイートを取得#til
ツイートがあればGitHub APIを叩いてコミットするアプリケーションの動きとしては上記の流れを想定します
なのでやらないといけないことは、
- Lambda Functionsの作成
- Twitter APIを叩いて
#til
ツイートを取得するjsを作る- GitHub APIを叩いてコミットするjsを作る
- Lambdaに登録する
- 定期的にLambda Functionsを実行するCloudWatch Eventsの作成
という感じになります
1. Lambda Functionsの作成
最終的にはこんなjsができあがりました
https://github.com/halnique/til-twitter/blob/master/index.js
色々詰め込み過ぎだし改善の余地は大いにありそうですが、中身を見ていきます
1-1. Twitter APIを叩いて
#til
ツイートを取得するTwitter開発者アカウントを登録してアプリ作成を行うと、以下の値が得られます
- Consumer API key
- Consumer API secret key
- Access token
- Access token secret
これらを環境変数から設定し、twitterモジュールを利用します
const Twitter = require('twitter'); const twitterClient = new Twitter({ consumer_key: process.env.API_KEY, consumer_secret: process.env.API_SECRET, access_token_key: process.env.ACCESS_TOKEN, access_token_secret: process.env.ACCESS_TOKEN_SECRET, });あとはTwitter APIのドキュメントを見ながら、特定のツイートを取得するように実装します
const nowString = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate() - 1}`; const params = { q: `(from:${process.env.ACCOUNT_NAME}) since:${nowString} ${TARGET_HASHTAG}`, count: process.env.MAX_COUNT || 5, }; const tweets = await twitterClient.get('search/tweets', params).catch(() => []);
ACCOUNT_NAME
は自分のTwitterアカウントを環境変数で設定します最終的には毎日日付が変わったタイミングぐらいで実行させるので、前日以降のツイートに絞っています
※ jsの日付処理貧弱すぎない?みんなmoment.jsとか使うの?
実際はこのあとに各ツイートのハッシュタグを厳密にチェックしてますが、だいたい↑ぐらいでお目当てのツイートは取得できるはずです
1-2. GitHub APIを叩いてPRを作成する
これが地味に大変だった…
CLIなら
git add
してgit commit
してgit push
するだけのかんたんなお仕事ですが、APIでやろうとするとある程度踏み入った理解が必要になります流れとしては
blob
を作ってtree
を作ってcommit
を作ってHEADのSHAを書き換える
ということになりますconst prevRefsHead = await getRefsHead(); const commitSha = prevRefsHead.object.sha; const prevCommit = await getCommit(commitSha); const blob = await postBlob(data[i]); const tree = await postTree(prevCommit.tree.sha, blob.sha, i + 1); const commit = await postCommit(commitSha, tree.sha, i + 1); await patchRefsHead(commit.sha); await sleep(1);中身はそれぞれ対応したAPIを実行しているだけですが、こちらもドキュメントとにらめっこしながらパラメータと流れを調節しました
※ 連続して実行したときにコミットがうまくいかないことがあったので、1件ずつsleepするようにしてます
1-3. Lambdaに登録する
jsは何度もお試し実行すると思うので、ローカルでDockerとかで書くのがよいです
jsができあがったらLambdaに登録していきます
注意点として、Lambda上で外部モジュールは基本的にそのまま
require
することはできません今回でいうと
const Twitter = require('twitter');ですね
これは事前に
Lambda Layers
として作成しておくことで解決できますzipファイルを直接アップロードするか、
S3
に上げておいてそれを利用することができますzipファイルの中身は注意が必要で、例えば
modules.zip
を解凍したときに以下の構成になっている必要があります$ ls -1 modules/ node_modules/
node_modules
の中に利用したいモジュールが入っているイメージですこの構成になっていないと、Lambda Functionsの方で利用するのにうまくいきません
先にLambda Layersを作ったら、Lambda Functionsを作成していきます
ランタイムはLambda Layersと同じになるようにします
Lambda FunctionsもzipファイルをアップロードしたりS3のファイルを利用することができますが、今回はそのまま作成したコードを貼り付けます
環境変数をたくさん使うので、ぽちぽち登録します
タイムアウト設定をデフォルトの3秒 -> 30秒に変更しておきます
最後にLambda FunctionsにLambda Layersを追加すればOKです
右上から適当なテストイベントを作ってテストしてみて、無事に動けばLambdaとしては完成です
2. 定期的にLambda Functionsを実行するCloudWatch Eventsの作成
以上
簡単に設定できました
Cron式については、日付が変わったころに前日分のツイートを取得するような感じで実行されるようにします
CloudWatch EventsのCron式で実行されるイベントは、UTCで誤差1分以内だそうです
前日に
#til
ツイートをしていれば、日本時間でだいたい翌日の朝9時頃にコミットができあがる想定ですね実行結果
コミットされました
これで無事にツイートするだけで草が生える環境ができあがりました
学び
- node_modulesを
Lambda Layers
に追加しておくことで、Lambda Functionsで使えるようになって便利- Gitのコミットができるまでの流れ 10.2 Git Internals - Git Objects
CloudWatch Events
のスケジュールでCron式を使う場合、日と曜日のどちらかは?
にする必要があるTodo
- Lambda FunctionsとLambda Layersへのデプロイを
GitHub Actions
で自動化したい人生だった…- GCPで
Cloud Functions
とCloud Scheduler
でも似たようなことができそうなのでやってみたいまとめ
GitHubの草が生えているからといって活発に開発をしているとは限らないぞ
- 投稿日:2019-12-15T19:06:27+09:00
Go、Node.jsのプログラム間でRPC通信をする
概要
gRPC
を使用して、Go、Node.jsのプログラム間でRPC通信をします。
クライアント側をGo、サーバ側をNode.jsが担当します。環境
MacOS Catalina: 10.15.1
Go: 1.13.4
Node.js: 10.15.3クライアント(Go)の作成
クライアント側のディレクトリを作成します。
$ mkdir grpc-test-goクライアント側のディレクトリ構成は最終的に以下のようになります。
$ cd grpc-test-go $ tree . ├── bridge │ ├── bridge.pb.go │ ├── bridge.proto │ └── go.mod ├── client.go └── go.mod.protoファイルの作成
protoファイルを作成して、仕様を定義します。
型にrepeated
をつけると配列になります。
公式ページを参照してください。bridge.protosyntax = "proto3"; package bridge; service BridgeService { rpc PostData (Data) returns (Reply) {} } message Data { string key = 1; repeated string data = 2; } message Reply { string response = 1; }.protoファイルからコードを生成
定義した.protoファイルからクライアント、サーバー共通で使用するコードを生成します。
まず、コードを生成するために必要なprotobuf
パッケージをインストールします。$ brew install protobuf$ protoc bridge/bridge.proto --go_out=plugins=grpc:.これで、
bridge.pb.go
が作成されましたmodule周りを整理
ローカルで
bridge.pb.go
を参照したいので、色々します。$ go mod init grpc-test-go $ cd bridge $ go mod init bridge
grpc-test-go
の方のgo.mod
ファイルを編集go.modmodule grpc-test-go go 1.13 require ( github.com/[username]/grpc-test2/bridge v0.0.0 google.golang.org/grpc v1.25.1 ) replace github.com/[username]/grpc-test-go/bridge => ./bridgeクライアント側コードの作成
client.gopackage main import ( "context" "fmt" "google.golang.org/grpc" pb "github.com/melonattacker/grpc-test-go/bridge" ) func RpcPost(key string, Data []string) (string, error) { conn, err := grpc.Dial("127.0.0.1:50051", grpc.WithInsecure()) if err != nil { return "", err } defer conn.Close() client := pb.NewBridgeServiceClient(conn) message := &pb.Data{Key: key, Data: Data} res, err := client.PostData(context.TODO(), message) response := res.Response if err != nil { return "", err } return response, nil } func main() { data := []string{"apple", "orange", "lemon"} result, err := RpcPost("fruit", data); if err != nil { fmt.Println(err) } fmt.Println(result) }$ go buildコンパイルが通るはずです。
サーバ(Node.js)の作成
サーバ側のディレクトリを作成します。
クライアント側と依存しない形で作成しましょう。$ mkdir grps-test-node $ cd grps-test-node.protoファイルの作成
上で作成した
bridge.proto
をコピーしてきます。bridge.protosyntax = "proto3"; package bridge; service BridgeService { rpc PostData (Data) returns (Reply) {} } message Data { string key = 1; repeated string data = 2; } message Reply { string response = 1; }必要なnpmパッケージのインストール
$ npm init -y $ npm install grpc @grpc/proto-loader --saveサーバ側コードの作成
server.jsconst grpc = require('grpc') const protoLoader = require('@grpc/proto-loader') const PROTO_PATH = __dirname + '/bridge.proto' const packageDefinition = protoLoader.loadSync( PROTO_PATH, { keepCase: true, longs: String, enums: String, defaults: true, oneofs: true } ) const BridgeProto = grpc.loadPackageDefinition(packageDefinition) const server = new grpc.Server() const PostData = (call, callback) => { console.log(call.request); callback(null, { response: "Data was sent to server with key: " + call.request.key }) } server.addService(BridgeProto.bridge.BridgeService.service, { PostData: PostData, }) server.bind('127.0.0.1:50051', grpc.ServerCredentials.createInsecure()) console.log('Listening on 127.0.0.1:50051...') server.start()実行
サーバ(Node.js)
$ cd grpc-test-node $ node main.js Listening on 127.0.0.1:50051...クライアント(Go)
$ cd grpc-test-go $ go run client.go
サーバ(Node.js)
{ data: [ 'apple', 'orange', 'lemon' ], key: 'fruit' }クライアント(Go)
Data was sent to server with key: fruit無事データが送られました!
以上です!
- 投稿日:2019-12-15T17:16:34+09:00
TwilioのAuthyの2FA(電話番号認証)機能をnode.jsから使う
呼び出し方は本家のAPI使う方法や先人のnpmパッケージ使う方法とかあるが、今回は先人のnpmパッケージを利用してみる。
ちなみにTwilioでは2つの電話番号認証機能があり、どちらを使うかはケースバーケースのようです。
Authy API
Authyが提供するAPIはSMS送信以外での2FAに対応していますが、今回はSMS送信による認証を試してみます。
準備
Twilioに登録
Twilioに登録して利用できるようにしておく。無料枠もあるがすぐに消費してしまいます・・・。
Authyのプロジェクト作成とAPIの取得
左メニューからAuthyを選択肢、新規プロジェクトを作成、設定にてPRODUCTION API KEYを取得します。
APIの機能(利用の流れ)
利用方法は簡単で、主な利用APIは下記の2つ。
- verification_start()でSMS経由でCode送信
- verification_check()でCode検証
あとは、
- verification_status()というAPIで状態を確認
することもできる。
実装
まず、モジュールをインストールします。
npm install --save authyで、実装。
以下のコードではめんどくさいの一気に記述していますが1)と2)は同時に実行することはできません。
まず、2)をコメントアウトして実行し、SMSが届くか見ましょう。そして、1)をコメントアウトして、Codeを設定した状態で2)をコメントインして実行します。//取得したAPIで初期化 var authy = require('authy')('MswmXqxxxxxxxxxxxxxxxxxxxxxxxxxxx'); const phoneNumber = '9012345678'; const countryCode = '81'; const verificationCode = '000000'; //受け取ったものを入れる //1)smsでcode送信 authy.phones().verification_start(phoneNumber, countryCode, { via: 'sms', locale: 'ja', code_length: '6' }, (err, res) => { if (err) throw err; console.log(res); }); //2)受け取ったコード認証 authy.phones().verification_check(phoneNumber, countryCode, verificationCode, (err, res) => { if (err) throw err; console.log(res); }); //3)ステータス確認 authy.phones().verification_status(phoneNumber, countryCode, (err, res) => { if (err) throw err; console.log(res); })スムーズにうまく動きました。
メモ
- 一度認証しても、再度送信するとstatusはpendingになるようです(まあ、当然ですが)。
- 料金は国内3キャリア向けに$0.08円(まあ10円)。
- あくまで電話番号の実在確認なので、会員管理システム等は別途用意しておく必要がある。
- 専門サービスだけあって090と記述しても送信してくれる。090-1234-5678とハイフンがあっても処理してくれる。
- 投稿日:2019-12-15T16:31:18+09:00
leapjs+johnny-fiveでクレーンゲームを操作する
はじめまして、@ufoo68です。普段はAWSとかReactを触る業務をやっておりますが、Qiitaでは色々と雑多なことを書いたりしております。
はじめに
今回は今更ながらLeap Motionを買ったのでこれをNode.jsのライブラリで遊んだりしておりました(何年前の記事だよと思われるかもしれませんが)。きっかけは私がとあるイベントでここ2年くらい前から展示している改造クレーンゲームがありまして↓
こいつは市販のやつにArduino Nanoを仕込んだものになってます。Arduinoの中にはFirmataを書き込むことで、PC上で動くのプログラミング言語で操作することができます。今まではPythonとOpenCVを使って、手のオブジェクト検出を使って操作するものを展示していたのですが、どうも照明とか外の光加減とか手の形の個人差とかで認識精度が左右されて調整とかが難しかったのでここは思い切って、Leap Motionを買うことにしました。
まずは動画
以下が実際に動作したものになります。
Leap Motionについて
Leap Motionを操作するためにleapjsというライブラリを用いました。今回は
palmPosition
という手のひらの位置を検出するメソッドを用いました。あと、注意点として、Leap Motionのドライバが認識しないという問題で躓いたりしたのでこういった情報を参考に調べるといいと思います。クレーンゲームについて
クレーンゲームというよりは、中に仕込んだArduinoについてですが、Firmataを書き込んでいるのでArduinoに毎回新しいソフトウェアを書き込む必要が無いです。このFirmataを書き込んだArduinoとUSB経由でPCと通信するわけですが、今回はJohnny-Fiveというライブラリを用いてNode.jsとArduinoを連動させました。
Leap MotionとArduinoを連動させる
今回はここのサイトを参考に実装しました。と言ってもまずは動くもの、という感じで書いたので以下のような雑な感じの実装になりました。
const Leap = require("leapjs") const five = require('johnny-five') const motor = { right: 6, left: 5, down: 2, up: 3, forward: 8, back: 9 } const board = new five.Board() board.on('ready', () => { const up = new five.Led(motor.up) const down = new five.Led(motor.down) const forward = new five.Led(motor.forward) const back = new five.Led(motor.back) const right = new five.Led(motor.right) const left = new five.Led(motor.left) const stop = () => { back.off() forward.off() down.off() up.off() right.off() left.off() } const controller = new Leap.Controller() controller.connect() controller.on('hand', hand => { console.log(hand.palmPosition) hand.palmPosition[0] > 0 ? right.on() : left.on() hand.palmPosition[1] > 150 ? up.on() : down.on() hand.palmPosition[2] < 40 ? forward.on() : back.on() setTimeout(stop, 500) }) })一応johnny-fiveはモーター動作をサポートしたライブラリもあるのですが、今回は単純なON/OFFでいいかなと思ったので
five.Led
を使っちゃいました。さいごに
もう少しソフトウェアとハードウェアのアップデートをして今年のNT京都2020に挑みたいと思います。一応今回はこのアドベントカレンダーへの間に合せということで。。。
一応ソースはGitHubで公開します。ではこのへんで、次は@kimamulaさんの投稿です。お楽しみに!
- 投稿日:2019-12-15T16:29:09+09:00
lambdaのNode.jsバージョンを上げるときはログのフォーマット変更にも注意
バージョンでログ出力が違うから注意
2019年末にlambdaのNode.js 8.10がEOLを迎えます。
ログ出力の部分で微妙に動作が違うので念の為確認してからバージョンアップしましょう。
特にログ出力をライブラリで行っている場合、そちらの実装がどうなっているか見ておいた方が良いです。
kibanaとかでパースするロジックに変更が必要になるかも。Node.js 8.10
関数コード
exports.handler = async (event) => { // TODO implement const response = { statusCode: 200, body: JSON.stringify('Hello from Lambda!'), }; // デフォルトのテンプレートにconsole.log入れただけ console.log(`"This is Node.js 8.10 log."`); return response; };Execution Result
Function Logs:
START RequestId: fcfedef6-ed16-4236-a407-5cc3d38bb2fe Version: $LATEST
2019-12-15T06:42:13.112Z fcfedef6-ed16-4236-a407-5cc3d38bb2fe "This is Node.js 8.10 log."
END RequestId: fcfedef6-ed16-4236-a407-5cc3d38bb2fe
REPORT RequestId: fcfedef6-ed16-4236-a407-5cc3d38bb2fe Duration: 0.49 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 58 MB
2019-12-15T06:42:13.112Z fcfedef6-ed16-4236-a407-5cc3d38bb2fe "This is Node.js 8.10 log."
が
実行日時 リクエストID console.logで出力した文字列
になってますね。
Node.js 10.x
関数コード
exports.handler = async (event) => { // TODO implement const response = { statusCode: 200, body: JSON.stringify('Hello from Lambda!'), }; console.log(`"This is Node.js 10.x log."`); return response; };Execution Result
Function Logs:
START RequestId: b7b8a64d-0bcf-405e-8257-0fc6d3f62419 Version: $LATEST
2019-12-15T06:46:11.635Z b7b8a64d-0bcf-405e-8257-0fc6d3f62419 INFO "This is Node.js 10.x log."
END RequestId: b7b8a64d-0bcf-405e-8257-0fc6d3f62419
REPORT RequestId: b7b8a64d-0bcf-405e-8257-0fc6d3f62419 Duration: 59.33 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 76 MB Init Duration: 174.25 ms
2019-12-15T06:46:11.635Z b7b8a64d-0bcf-405e-8257-0fc6d3f62419 INFO "This is Node.js 10.x log."
が
実行日時 リクエストID ログレベル console.logで出力した文字列
と、出力されるようになっています。
8.10と比較するとログレベルが出力されるようになっていることに注意しましょう。Node.js 12.x
Execution Result
Function Logs:
START RequestId: 786d807b-6bb9-4267-88f7-4df1f4955a7f Version: $LATEST
2019-12-15T06:47:52.767Z 786d807b-6bb9-4267-88f7-4df1f4955a7f INFO "This is Node.js 12.x log."END RequestId: 786d807b-6bb9-4267-88f7-4df1f4955a7f
REPORT RequestId: 786d807b-6bb9-4267-88f7-4df1f4955a7f Duration: 2.80 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 70 MB Init Duration: 111.41 ms
2019-12-15T06:47:52.767Z 786d807b-6bb9-4267-88f7-4df1f4955a7f INFO "This is Node.js 12.x log."END RequestId: 786d807b-6bb9-4267-88f7-4df1f4955a7f
が
実行日時 リクエストID ログレベル console.logで出力した文字列END RequestId: リクエストID
と、10.xとも違う、おそらくAWS側も意図していない動作をしてるように見受けられます。
(コピペミスとかでもなかったです)とはいえ、CloudWatchには意図どおりに出力されてそうなので特に問題にはならないと思います。
補足
consoleオブジェクトの関数に応じてログレベルを出力するようになっているようです。
Node.js 8.10
関数コード
exports.handler = async (event) => { // TODO implement const response = { statusCode: 200, body: JSON.stringify('Hello from Lambda!'), }; console.log(`"This is Node.js 8.10 log."`); console.info(`"This is Node.js 8.10 info log."`); console.warn(`"This is Node.js 8.10 warn log."`); console.error(`"This is Node.js 8.10 error log."`); return response; };Execution Result
Function Logs:
START RequestId: c2a0c056-7623-4c4f-bbad-6b458800cd7d Version: $LATEST
2019-12-15T07:18:36.692Z c2a0c056-7623-4c4f-bbad-6b458800cd7d "This is Node.js 8.10 log."
2019-12-15T07:18:36.692Z c2a0c056-7623-4c4f-bbad-6b458800cd7d "This is Node.js 8.10 info log."
2019-12-15T07:18:36.692Z c2a0c056-7623-4c4f-bbad-6b458800cd7d "This is Node.js 8.10 warn log."
2019-12-15T07:18:36.692Z c2a0c056-7623-4c4f-bbad-6b458800cd7d "This is Node.js 8.10 error log."
END RequestId: c2a0c056-7623-4c4f-bbad-6b458800cd7d
REPORT RequestId: c2a0c056-7623-4c4f-bbad-6b458800cd7d Duration: 0.50 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 58 MBログを見てもどんなレベルのログかが分からない。
Node.js 10.x
関数コード
exports.handler = async (event) => { // TODO implement const response = { statusCode: 200, body: JSON.stringify('Hello from Lambda!'), }; console.log(`"This is Node.js 10.x log."`); console.info(`"This is Node.js 10.x info log."`); console.trace(`"This is Node.js 10.x trace log."`); console.debug(`"This is Node.js 10.x debug log."`); console.warn(`"This is Node.js 10.x warn log."`); console.error(`"This is Node.js 10.x error log."`); console.fatal(`"This is Node.js 10.x fatal log."`); return response; };Execution Result
Function Logs:
START RequestId: d12b8a86-62eb-4caa-9d09-ed66b1ec08ed Version: $LATEST
2019-12-15T07:15:02.460Z d12b8a86-62eb-4caa-9d09-ed66b1ec08ed INFO "This is Node.js 10.x log."
2019-12-15T07:15:02.465Z d12b8a86-62eb-4caa-9d09-ed66b1ec08ed INFO "This is Node.js 10.x info log."
2019-12-15T07:15:02.465Z d12b8a86-62eb-4caa-9d09-ed66b1ec08ed TRACE "This is Node.js 10.x trace log."
2019-12-15T07:15:02.465Z d12b8a86-62eb-4caa-9d09-ed66b1ec08ed DEBUG "This is Node.js 10.x debug log."
2019-12-15T07:15:02.465Z d12b8a86-62eb-4caa-9d09-ed66b1ec08ed WARN "This is Node.js 10.x warn log."
2019-12-15T07:15:02.465Z d12b8a86-62eb-4caa-9d09-ed66b1ec08ed ERROR "This is Node.js 10.x error log."
2019-12-15T07:15:02.465Z d12b8a86-62eb-4caa-9d09-ed66b1ec08ed FATAL "This is Node.js 10.x fatal log."
END RequestId: d12b8a86-62eb-4caa-9d09-ed66b1ec08ed
REPORT RequestId: d12b8a86-62eb-4caa-9d09-ed66b1ec08ed Duration: 47.48 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 76 MB Init Duration: 151.14 msどんなレベルのログが一目瞭然だし、ログのパースするときに便利そう。
8.10と比較すると種類も増えてます。他にもあるかも。リンク
- 投稿日:2019-12-15T15:18:11+09:00
金曜の夜になったら会社の Slack 通知を自動でミュートしたい
はじめに
「休日は会社の Slack をミュートしておきたい!」という要望は普通にあると思うのですが、
2019年12月15日現在、Slackには特定の曜日に自動で「おやすみモード」にする機能はありません。そこで色々と試してみたのですが Zapier(または IFTTT)で Slack API を叩く方法が無料かつ最も簡単にできたので、
本記事ではその手順を解説していきます。手順
まず大まかにやることをまとめると、
- Slack 側で API を利用できるよう設定する
- IFTTT / Zapier 側で「時刻が金曜の21時であるとき」「Webhook / JavaScriptから API を叩く」アプレットを作る
の2つです。
Slack側の設定
Slack 側では Web API の利用を許可し OAuth トークンを取得する必要があります。
そのためには Slack App を作成しなくてはならないので、以下では必要最低限な App の作り方を解説します。
まず Slack API のページにアクセスし、中央の [Start Building] ボタンを押しましょう。
APP 名を適当に入力し、どのワークスペースに作成するかを選択後、右下の [Create App] を押します。
すると何かゴチャゴチャしたページに飛ぶので、Add features and functionality 節の右下辺りの [Permissions] を選択します。
このページでどういう API の使用を許可するのかを設定します。
Scope 節の [Add an OAuth Scope] ボタンを押しましょう。
(大きい緑のボタンではなく下の白いボタンの方なので注意)今回利用するAPI の仕様によると、権限として dnd:write が必要と書いてあるので、検索し選択します。
その後ページの一番上に戻ると、App がインストールできるようになっています。
インストールに成功すると API を叩くのに必要な OAuth トークンが表示されます。
このトークンを知っている人は誰でも API を叩けるので管理には一定の注意が必要です。
(今回の場合はおやすみモードの切り替えができるだけと思いますが一応)Slack 側の設定はこれで完了です。トークンだけ後々使用します。
IFTTT / Zapier側の手順
IFTTT と Zapier、どちらを選ぶべきか
どちらも似たようなサービスですが、基本的にはZapierの方が高機能と言えます。
IFTTT はアクションが一つしか登録できないなど制限は多いですが、
Webhook が無料で利用でき UI もわかりやすいため、単純な用途であれば IFTTT をオススメします。Zapier はよりカスタマイズ性が高く、何よりNode.js や Python のコードを実行することができます。
機能を細かく調整したい場合や拡張性を持たせたい場合は Zapier がいいと思います。IFTTT の Webhook の使い方はググればたくさん出てくると思うので、
本記事では Zapier の Node.js からAPIを叩く方法をご紹介します。Zapierの手順
Zapier への登録方法は割愛します。
ログイン後、右上の [Make a Zap!] ボタンを押してください。
まずはトリガーとなる App を選択してと言われるので、時刻をトリガーとする [Schedule by Zapier] を選択します。
次に、どういう条件でトリガーさせるかを聞かれます。
トリガーさせたい曜日が1つだけの場合は every week, 複数ある場合は everyday を選択するといいです。
今回は説明のため everyday を選択します。
上で everyday を選択するとトリガーさせる時刻を聞かれます。
金曜の午後9時以降は会社のことを忘れたいので「9pm」を選択し、CONTINUE します。次の画面で [TEST & CONTINUE] というボタンが出るので、押下してトリガーの設定を完了します。
次はアクションの設定です。下の [Do this...] をクリックしましょう。
App は Code by Zapier を選択します。
Node.js の ver 10.x.x を選択します。
次に実行したいコードを記述していくのですが、その前に Zapier の Code のしくみをざっくり解説します。
Input Data
Code の設定項目には「Input Data」と「Code」があるのですが、まずはInput Dataから説明していきます。
Input Dataでは、一つ前のトリガーやアクションからどういうデータをどのように受け取るかを設定します。
具体的にはここで「データのプロパティ名」と「データの種類」を入力しておくと、
コード内でinputData.プロパティ名
の形でそのデータを取得できるようになります。今回はスケジュールトリガーから曜日データを受けとりたいので、Pretty Day Of Week(整形された曜日情報)を、
dayOfWeek
のプロパティ名で受け取れるように設定しています。Code
Code 節には実行するコードを書くのですが、少しクセがあって、
- コード全体が async function にラップされている
- オブジェクトまたはオブジェクトの配列を
output
という定義済み変数に入れなければならないとなっています。
output
に入ったものが次のアクションに渡されるしくみになっていて、
次のアクションがない場合でも必ず値を入れなければなりません。また async function なので
await
を使用することが可能です。
というか非同期の場合にawait
を使わないとoutput
に何も入ってないよ!とエラーになる可能性があります。実際のコードは以下のようになりました。
codeconst https = require('https'); const endpoint = 'https://slack.com/api/dnd.setSnooze' const token = '<Slackで取得したトークンを入れてね!>' if(inputData.dayOfWeek === 'Friday') { const numMinutes = 60 * (48 + 9) // 2 days and 9 hours. const url = `${endpoint}?token=${token}&num_minutes=${numMinutes}` // オブジェクトまたはオブジェクトの配列を確実に返さないとエラーになる output = await fetch(url).catch(error => { return error }) }※細かいことを言えば、トークンを直書きしているのでZapier の中の人が見ようと思えば見ることができます。それが気になる方はセキュリティ強化された Zapier Platform を利用するなどしてください。
上記コードを入力後、[CONTINUE] を押して次のような表示が出れば・・
あとは作ったアプレットを ON にするだけで全て完了です!
お疲れ様でした。補足
シンプルな要件のわりにはそれなりに実現が大変でした。公式で機能を作ってくれるといいですね。
今回の方法だと週末以外の休日には対応できませんが、カレンダーと連携させるとより柔軟にミュートができるかもしれません。
- 投稿日:2019-12-15T15:05:18+09:00
Node.jsからSendGridを使ってメールを送る
久しぶりにSendGridを使ってみたのでメモ。
SendGridの注意点
- ユーザ名はメールアドレスではなく、代理店である構造計画研究所から独自に振られたxxx@kke.comというやつ
久しぶりですっかりID忘れてました。
準備
API KEYの取得
利用するにはAPI KEYが必要です。到達率を上げるためにはドメイン認証やらいろいろやったほうがいい。
API KEYはSettingsの中にある。作業場の準備
mkdir sendmail cd sendmail touch index.js npm init -f npm install --save @sendgrid/mail実装
難しくない。本家サイトにユースケースの紹介があるのでそれを見ながら実装する。
まずはSend a sigle email to single recipientを試す。API_KEYやらその他情報は各自環境に合わせて変更。
const sgMail = require('@sendgrid/mail'); const API_KEY = "XX.O1c1v3QNQzed41lkvQKNIw.7DYw-coFLLfoLqEuWADNzKH_xxxxxxxxxxxxxxxxxx"; sgMail.setApiKey(API_KEY); const msg = { to: 'to@mail.com', from: 'from@mail.com', subject: 'test mail from sg', text: 'hoge hoge', html: '<p>foo bar</p>' } sgMail.send(msg).then(res => { console.log(res); }).catch(e => { console.log(e); });複数人に送る場合はアドレスの配列を作り、sgMail.sendMultiple(msg)としてやるみたい。
動作確認
実行してみる。
node index.js届いたみたい。
- 投稿日:2019-12-15T14:17:49+09:00
Temporal dead zoneと死の秘宝
ChromeやNodeJSのJavascriptコンソール画面で動作確認する場合、
以下の様に間違ってエラーになってしまうことがあります。const obj = JSON.parse(""); // JSON形式じゃない文字列を指定 // Uncaught SyntaxError: Unexpected end of JSON inputJSON形式の文字列で指定するところに空文字を指定した場合ですが、
Uncaught SyntaxError
となってしまいます。じゃあ間違えたのだからと訂正して再度実行すると
const obj = JSON.parse("[]"); // JSON形式の文字列を指定 // Uncaught SyntaxError: Identifier 'obj' has already been declared既に宣言済みなのでエラーとなります。
それでは、既に宣言済みなら変数が存在するのだと思って参照してみると
console.log(obj); // Uncaught ReferenceError: obj is not defined宣言されていないとエラーとなります。
グローバルに変数が残っているかなと思って
delete
を実行しても消えている様子は無いです。delete obj; // false const obj = JSON.parse("[]"); // Uncaught SyntaxError: Identifier 'obj' has already been declaredMDNのlet変数やconst変数の説明を見てみるとTemporal dead zoneの存在について紹介されています。
undefined の値で始まる var 変数と異なり、 let 変数は定義が評価されるまで初期化されません。変数を宣言より前で参照することは ReferenceError を引き起こします。ブロックの始めから変数宣言が実行されるまで、変数は "temporal dead zone" の中にいるのです。
どうやら、宣言された
const
やlet
変数はTemporal dead zoneの中に残ってしまっていて参照できない状態になっているようです。エラーが発生するなどして変数の初期化が行われなかった場合、
let
やconst
についてはTemporal dead zoneに変数があるため、宣言はされているが、参照出来ない状態となるらしいです。この状態だと変数を再使用することは出来なくなってしまいます。
- let - JavaScript | MDN
- Chrome console already declared variables throw undefined reference errors for let
対応策としては、
リロードなどをして最初から実行し直すか、
もしくは、var
を使うか、もしくはスコープを指定するか、
{ const obj = JSON.parse(""); }もしくは
let
を使っていったん宣言だけすれば最初にundefined
がセットされます。let obj; // undefined obj = JSON.parse(""); // Uncaught SyntaxError: Unexpected end of JSON input obj = JSON.parse(""); // Uncaught SyntaxError: Unexpected end of JSON input闇の魔術とはちょっと違うかも知れないけれど、Temporal dead☠️ zoneの呼び方が闇っぽいのでここに載せておきました。
と思っていたら、The Temporal Dead Zone and the Deathly Hallows(Temporal Dead Zoneと死の秘宝)と呼んでいる記事があったのでタイトルも合わせて変えました。
- 投稿日:2019-12-15T14:17:49+09:00
Temporal dead zoneと消えない変数
ChromeやNodeJSのJavascriptコンソール画面で動作確認する場合、
以下の様に間違ってエラーになってしまうことがあります。const obj = JSON.parse(""); // JSON形式じゃない文字列を指定 // Uncaught SyntaxError: Unexpected end of JSON inputJSON形式の文字列で指定するところに空文字を指定した場合ですが、
Uncaught SyntaxError
となってしまいます。じゃあ間違えたのだからと訂正して再度実行すると
const obj = JSON.parse("[]"); // JSON形式の文字列を指定 // Uncaught SyntaxError: Identifier 'obj' has already been declared既に宣言済みなのでエラーとなります。
それでは、既に宣言済みなら変数が存在するのだと思って参照してみると
console.log(obj); // Uncaught ReferenceError: obj is not defined宣言されていないとエラーとなります。
グローバルに変数が残っているかなと思って
delete
を実行しても消えている様子は無いです。delete obj; // false const obj = JSON.parse("[]"); // Uncaught SyntaxError: Identifier 'obj' has already been declaredMDNのlet変数やconst変数の説明を見てみるとTemporal dead zoneの存在について紹介されています。
undefined の値で始まる var 変数と異なり、 let 変数は定義が評価されるまで初期化されません。変数を宣言より前で参照することは ReferenceError を引き起こします。ブロックの始めから変数宣言が実行されるまで、変数は "temporal dead zone" の中にいるのです。
どうやら、宣言された
const
やlet
変数はTemporal dead zoneの中に残ってしまっていて参照できない状態になっているようです。エラーが発生するなどして変数の初期化が行われなかった場合、
let
やconst
についてはTemporal dead zoneに変数があるため、宣言はされているが、参照出来ない状態となるらしいです。この状態だと変数を再使用することは出来なくなってしまいます。
- let - JavaScript | MDN
- Chrome console already declared variables throw undefined reference errors for let
対応策としては、
リロードなどをして最初から実行し直すか、
もしくは、var
を使うか、もしくはスコープを指定するか、
{ const obj = JSON.parse(""); }もしくは
let
を使っていったん宣言だけすれば最初にundefined
がセットされます。let obj; // undefined obj = JSON.parse(""); // Uncaught SyntaxError: Unexpected end of JSON input obj = JSON.parse(""); // Uncaught SyntaxError: Unexpected end of JSON input闇の魔術とはちょっと違うかも知れないけれど、Temporal dead☠️ zoneの呼び方が闇っぽいのでここに載せておきました。
と思っていたら、The Temporal Dead Zone and the Deathly Hallows(Temporal Dead Zoneと死の秘宝)と呼んでいる記事があった。
- 投稿日:2019-12-15T12:41:49+09:00
reveal.jsの環境構築で躓いた話(windows 10)
はじめに
最近、私の所属しているサークルでLT会をやっていこうという流れがあったので、Markdownからスライドを作れる「reveal.js」について紹介しました。
そのためにreveal.jsをセットアップしようとしたのですが、公式ドキュメントでは3行だったのが1時間以上かかってしまったので、その備忘録です。Markdownとは
markdownは簡単な記法で記事などの文章を構成出来るマークアップ言語です。
qiitaやはてなブログなどで記事を上げている諸兄には馴染み深いものかもしれません。記述例:
# 見出し1 ## 見出し2 --- 線 **強調**実行例:
見出し1
見出し2
強調
reveal.jsとは
かっこいいスライドを簡単に作れるフレームワークです。
htmlに書き込んで作りますが、ちょちょっと設定すると、Markdownを書くだけでそれがスライドになります。
パワーポイントをマウスクリックしながら無限に時間を溶かすことも、揃ってないデザインを先輩に怒られることもなくなります。セットアップする
reveal.jsのセットアップには2つの方法があります。
1) 簡単に使うだけなら、githubのページからプロジェクトをローカルに保存するだけで大丈夫です。
2) 拡張マークダウンを使ったり、スピーカーノート(発表者のメモを隣に表示する機能)を使う場合には、Node.jsを使いローカルにサーバーを建てるFull Setupが必要です。
今回はこの(2)のFull Setupの方法を解説します。
1. reveal.js をクローン OR ダウンロードする。
2. Node.js をインストールする
公式サイトからNode.jsをダウンロード&インストールします。
3. npmを使ってインストールします
やることは、Node.js についてくるパッケージマネージャnpmを使って環境を整えるだけです。
package.jsonファイルに設定はしてくれてあるので、これに従うだけです。カレントディレクトリをreveal.jsのフォルダに移動してから
$ npm install $ npm startでサーバーをインストールして起動して終了なんですが、そうは問屋がおろしませんでした。
4.MSBuild.exeに関するエラーが出たら
VisualStudioを入れていないか、versionが異なっています。
もしVSを入れていなかったら入れましょう。
または、VS2019を入れていてエラーが出たらバージョンの設定が違うので、$ npm config set msvs_version 2019でコンフィグを設定します。
5. Windows Build Toolsをインストールする
Windows-Build-Toolsが入っていない場合はこれをインストールします
$ npm install -g windows-build-toolsこれでOKです。
6. npm-check-updatesを入れる
パッケージを確認するためにnpm-check-updatesを入れて、パッケージのバージョンを整理してもらいます。
$ npm install -g npm-check-updatesncuコマンドを使います
npm-check-updatesを実行するコマンド、ncuを実行します
$ ncuするとncu -uをしてアップグレードをするように促されるので、仰せの通りにします。
$ ncu -u7. npm installをしなおす
これでnpm installをするように促されるので、仰せの通りにします。
$ npm install「Binary is fine」と出れば成功です!
reveal.js を使う
インストールが終わったのでnpm startでサーバーを起動します。
$ npm startブラウザで見る
これで http://localhost:8000 にサーバーが立っています。
晴れて、reveal.jsを使えるようになりました!終わりに
ここでは、Windows10でつまづきまくった環境構築について説明しました。
この記事ではその使い方、Markdownの書き方までは解説しなかったです。
(これについては非常に分かりやすい記事が沢山あるので)]以下の記事をご参考にしてください。
reveal.jsでスライド作り。reveal.jsを使うと非常に楽にスライドを作ることが出来るので、是非是非活用してください
- 投稿日:2019-12-15T12:41:31+09:00
Node.jsでのCLIの作り方と便利なライブラリまとめ
はじめに
Node.jsで
CLI(Command Line Interface)
を作りたくなることがあると思います。
そして、GitHubに公開されているCLIを見ると、色々なライブラリを組み組み合わせて便利なCLIを作っているようです。この記事では、Node.jsでCLIをどう作るのか?そして、CLI開発を支える便利なライブラリを紹介します。
身の回りのCLI
CLIの作り方を見る前に、普段の開発で触れているCLIを見てみましょう。
ESLint
CLIには基本的に
--help
オプションが用意されていますね。
npm
expo
プレースホルダーがあることで入力する内容のイメージを伝えることができます。
stencil
様々な選択方法をユーザーに提供したり、分かりやすく色付けすることも可能です。
Node.jsでのCLIの作り方
それでは、Node.jsでCLIを作っていきます。
一般的にCLIの開発では便利なライブラリを使いますが、今回は汎用的な知識としてライブラリを使わずに標準モジュールだけで開発します。
ここでは、以下のような引数を1つ受け取り、ユーザーの入力を受け取るCLIを作ります。
引数の受け取り
Node.jsで
process.argv
はコマンドライン引数を含む配列を返します。この配列の3つ目からの要素にコマンドライン引数が格納されています。Node.js Documentation | process.argv
// lib/index.js console.log(process.argv[2]); console.log(process.argv[3]);$ node lib/index.js foo bar foo barここから実際に作成するCLIのコードを書いていきます。
1つの引数を必ず受け取るようにチェックしつつ、受け取った値を使ってメッセージを表示します。
lib/index.js
const [, , firstArg] = process.argv; if (!firstArg) { console.error("Please pass one argument!!"); process.exit(1); } const msg = ` Hello!! ${firstArg} san. I am Toshihisa Tomatsu. GitHub: https://github.com/toshi-toma Twitter: https://twitter.com/toshi__toma `; console.log(msg);$ node lib/cli.js tom Hello!! tom san. I am Toshihisa Tomatsu. GitHub: https://github.com/toshi-toma Twitter: https://twitter.com/toshi__tomaユーザーの入力を受け取る
次はCLIでよくあるユーザーの入力を受け取れるようにしましょう。ここでは組み込みのモジュール
readline
を使います。Node.js Documentation | Readline
また、
readline
のquestion
関数を利用すると、ユーザーへのプロンプトメッセージの表示と、ユーザー入力の受け取りまでを行うことができ便利です。
lib/index.js
// ... const readline = require("readline"); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.question("Please enter names for your project: ", answer => { console.log(`Thank you!! Let's start ${answer}`); rl.close(); });node bin/cli.js tom Hello!! tom san. I am Toshihisa Tomatsu. GitHub: https://github.com/toshi-toma Twitter: https://twitter.com/toshi__toma Please enter names for your project:
ユーザーの入力を受け取り、それを使ったメッセージの表示まで行えました。
動作確認
CLIの作成は行えましたが、実際にユーザーが利用する場合、
eslint file1.js file2.js
だったりnpm init
といった形式で利用します。ここでは実際のCLIのように実行できるようにします。
npm init
まずは
package.json
を用意する必要があるので、npmのinit
コマンドで作成します。$ npm init -ypackage.json bin
package.json
のbin
フィールドで、コマンドとファイルのマッピングを行えます。こうしておくことでパッケージのインストール時にglobal installやlocal installで適切な場所にシンボリックリンクを作成します。
今回は、
bin/cli.js
をコマンド実行用に用意します。{ // ... "bin": { "cli": "bin/cli.js" }, // ... }bin/cli.js
ファイルの先頭に
#!/usr/bin/env node
をつけるのを忘れないように。
bin/cli.js
#!/usr/bin/env node require("../lib/index")();先程作成した
lib/index.js
を外部から利用できるようにmodule化しておきます。
lib/index.js
const readline = require("readline"); module.exports = () => { const [, , firstArg] = process.argv; if (!firstArg) { console.error("Please pass one argument!!"); process.exit(1); } const msg = ` Hello!! ${firstArg} san. I am Toshihisa Tomatsu. GitHub: https://github.com/toshi-toma Twitter: https://twitter.com/toshi__toma `; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); console.log(msg); rl.question("Please enter names for your project: ", answer => { console.log(`Thank you!! Let's start ${answer}`); rl.close(); }); };npm link
コマンドを用意できたので手元で試してみます。
ここではnpm link
を使うと便利です。$ npm link audited 1 package in 0.951s found 0 vulnerabilities /usr/local/bin/cli -> /usr/local/lib/node_modules/@toshi-toma/cli/bin/cli.js /usr/local/lib/node_modules/@toshi-toma/cli -> /Users/toshi-toma/dev/github.com/toshi-toma/cliこうすることで先程用意した
cli
コマンドを実行することができます。$ cli Please pass one argument!!npm publish
最後に、誰でもこのコマンドが使えるようにnpmにpublishします。
今回は自分用に作っただけなので、
scoped package
として公開します。まず、npmにログイン済みなことを確認してください。もしアカウントを持ってない人は、アカウントを作成して、ログインを行ってください。
Creating a new user account on the public registry
$ npm whoami toshi-tomaあとは
package.json
のname
とpublishConfig
を指定します。
name
は@<ユーザー名>/パッケージ名
とします。{ "name": "@toshi-toma/cli", "publishConfig": { "access": "public" }, // ... }最後に
npm publish
コマンドを実行すれば、@<ユーザー名>/パッケージ名
としてパッケージが公開されます。npx @toshi-toma/cli tom Hello!! tom san. I am Toshihisa Tomatsu. GitHub: https://github.com/toshi-toma Twitter: https://twitter.com/toshi__toma Please enter names for your project:
便利なライブラリ
Node.jsでCLIを作る方法を紹介しましたが、特に何もライブラリを使わずに標準モジュールだけで作成しました。
process.argv
やreadline
だと実装が複雑になったり面倒です。
また実際はコマンドライン引数のパースやオプション、バリデーション、helpの作成など複雑な処理を実装することになります。それを簡単に実装できるライブラリを使うのが一般的なようです。ここからは、CLI作成に便利なライブラリを紹介します。
コマンドの作成や引数のパース
yargs
https://github.com/yargs/yargs
yargsはコマンドやオプションの作成及び引数のパース、helpの自動作成などCLI作成を便利に行えるライブラリです。
require("yargs") .scriptName("console") .usage("$0 <cmd> [args]") .command( "hello [name]", "console your name!", yargs => { yargs.positional("name", { type: "string", default: "Toshihisa", describe: "the name to say hello to" }); }, function(argv) { console.log("hello", argv.name, "welcome to yargs!"); } ) .help().argv;$ node lib/yargs.js --help console <cmd> [args] コマンド: console hello [name] console your name! オプション: --version バージョンを表示 [真偽] --help ヘルプを表示 [真偽]minimist
https://github.com/substack/minimist
minimistはコマンドライン引数のパースを行ってくれるシンプルなライブラリです。
const argv = require("minimist")(process.argv.slice(2)); console.log(argv);$ node lib/minimist.js src -a bar --watch { _: [ 'src' ], a: 'bar', watch: true }cac
cacはCLI作成に必要な機能が実装されたシンプルなライブラリです。option、version、help、parseといった4つのAPIについて知るだけで使えるので非常に簡単です。
const cli = require("cac")(); cli.option("--type [type]", "Choose a project type", { default: "node" }); cli.option("--name <name>", "Provide your name"); cli.command("lint [...files]", "Lint files").action((files, options) => { console.log(files, options); }); cli.help(); cli.version("0.0.0"); cli.parse();$ node lib/cac.js --help cac.js v0.0.0 Usage: $ cac.js <command> [options] Commands: lint [...files] Lint files For more info, run any command with the `--help` flag: $ cac.js lint --help Options: --type [type] Choose a project type (default: node) --name <name> Provide your name -h, --help Display this message -v, --version Display version numbercommander
https://github.com/tj/commander.js
commanderはとても有名で使われているCLI作成に必要なAPIが用意されたライブラリです。
const program = require("commander"); program .command("clone <source> [destination]") .description("clone a repository into a newly created directory") .action((source, destination) => { console.log("clone command called"); }); program .version("0.1.0") .command("install [name]", "install one or more packages") .command("list", "list packages installed", { isDefault: true }) .parse(process.argv);meow
https://github.com/sindresorhus/meow
meowはテンプレートリテラルを使ったとてもシンプルにCLIを作成できるライブラリです。
const meow = require("meow"); const foo = require("."); const cli = meow( ` Usage $ foo <input> Options --rainbow, -r Include a rainbow Examples $ foo unicorns --rainbow ? unicorns ? `, { flags: { rainbow: { type: "boolean", alias: "r" } } } ); console.log(cli); foo(cli.input[0], cli.flags);色付け
chalk
https://github.com/chalk/chalk
chalkは以下のように
chalk.red("文字列")
とするだけで色付けが行えます。
また、chalk.blue.bgRed.bold("Hello world!")
のように必要なスタイルをチェーンできるのも直感的で簡単です。似たライブラリにkleurがあります。
const chalk = require("chalk"); console.log(chalk.blue("Hello") + " World" + chalk.red("!")); console.log(chalk.blue.bgRed.bold("Hello world!")); console.log(chalk.blue("Hello", "World!", "Foo", "bar", "biz", "baz")); console.log(chalk.red("Hello", chalk.underline.bgBlue("world") + "!"));UI
ora
https://github.com/sindresorhus/ora
oraを使えば、綺麗なスピナーが簡単に表示できます。
const ora = require("ora"); const spinner = ora("Loading unicorns").start(); setTimeout(() => { spinner.color = "yellow"; spinner.text = "Loading rainbows"; }, 1000);clui
https://github.com/nathanpeck/clui
cluiはコマンドラインのUIツールキットで、ゲージやスピナー、プログレスバーなどを簡単に表示することができます。
const Spinner = require("clui").Spinner; let countdown = new Spinner("Exiting in 5 seconds... ", [ "⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷" ]); countdown.start(); let number = 5; setInterval(function() { number--; countdown.message("Exiting in " + number + " seconds... "); if (number === 0) { process.stdout.write("\n"); process.exit(0); } }, 1000);figlet
https://github.com/patorjk/figlet.js
figletはテキストからアスキーアートを作成できるライブラリです。
const figlet = require("figlet"); figlet("Hello World!!", function(err, data) { console.log(data); });$ node lib/figlet.js _ _ _ _ __ __ _ _ _ _ | | | | ___| | | ___ \ \ / /__ _ __| | __| | | | | |_| |/ _ \ | |/ _ \ \ \ /\ / / _ \| '__| |/ _` | | | | _ | __/ | | (_) | \ V V / (_) | | | | (_| |_|_| |_| |_|\___|_|_|\___/ \_/\_/ \___/|_| |_|\__,_(_|_)update-notifier
https://github.com/yeoman/update-notifier
update-notifierを使えばアップデート情報のボックスを簡単に表示することができます。
terminal-image
https://github.com/sindresorhus/terminal-image
ターミナルに画像を表示することができます。
terminal-link
https://github.com/sindresorhus/terminal-link
ターミナルでリンクを作成することができます。
log-symbols
https://github.com/sindresorhus/log-symbols
ログレベルを表現する時に便利です。info、success、warning、errorが用意されています。
その他
ink
https://github.com/vadimdemedes/ink
inkは
React
でCLIを作成できるライブラリです。Gatsby
やParcel
でも利用されているようです。import React from "react"; import { render, Box } from "ink"; const Demo = () => <Box>Hello World</Box>; render(<Demo />);shelljs
https://github.com/shelljs/shelljs
shelljsはその名の通り、Node.jsから簡単にUnixシェルコマンドを利用できます。Windows/Mac/Linuxでポータブルに動作するのも便利です。
const shell = require("shelljs"); console.log(shell.which("git")); console.log(shell.cat("package.json")); shell.cp("package.json", "package-copy.json"); shell.ls("lib/**/*.js").forEach(function(file) { console.log(file); });clear
https://github.com/bahamas10/node-clear
clearを使えば、ターミナルの画面を一旦まっさらにすることができます。
const clear = require("clear"); clear(); console.log("Hello clear");inquirer
https://github.com/SBoudrias/Inquirer.js/
inquirerはインタラクティブなCLIのインターフェイスを作成できるライブラリです。回答の方法は入力、リストやチェックボックス、パスワード形式など、様々な方法が用意されています。
似たライブラリでEnquirerやpromptsがあります。
const inquirer = require("inquirer"); inquirer .prompt([ { name: "name", message: "What's your name?", default: "toshi-toma" }, { type: "list", name: "job", message: "What is your occupation?", choices: ["Frontend", "Backend", "Infra"] }, { type: "checkbox", name: "country", message: "Where are you from?", choices: ["Japna", "US", "China", "Others"] } ]) .then(({ name, job, country }) => { console.log(name); console.log(job); console.log(country); });listr
https://github.com/SamVerschueren/listr
listrは任意のタスクリストのステータスや進捗を表示することができるライブラリです。タスクを
Listr
の配列に渡すだけです。const Listr = require("listr"); const tasks = new Listr([ { title: "Task 1", task: () => Promise.resolve("Foo") }, { title: "Can be skipped", skip: () => { if (Math.random() > 0.5) { return "Reason for skipping"; } }, task: () => "Bar" }, { title: "Task 3", task: () => Promise.resolve("Bar") } ]); tasks.run().catch(err => { console.error(err); });$ node lib/listr.js ✔ Task 1 ↓ Can be skipped [skipped] → Reason for skipping ✔ Task 3oclif、gluegun
https://github.com/oclif/oclif
https://github.com/infinitered/gluegunCLIを作成するフレームワークもあるようです。
まとめ
Node.jsでシンプルなCLIの作成方法から、CLI作成を簡単に行える便利なライブラリを紹介しました。
自分でも調べてみて、便利なライブラリや似たライブラリがとても多く、実際どれを使えばいいのか分かりませんでした。
だいたいできることは同じなので、サンプルコードを見て、好みで使ってみるのがいいと思います。そして、安定のsindresorhusがとても便利なライブラリをたくさん作成してくれていることが分かります。
- 投稿日:2019-12-15T07:34:58+09:00
続・実録 Node-REDノード作成 24時
こんにちは、ポキオです。
IoTLT Advent Calendar 2019とenebular Advent Calendar 2019の15日目の記事です。
手抜きです、ごめんなさい。tl;dr
- 京急ノードを作ってみました
- Node-REDのノードライブラリに反映されるまで時間がかかることがあります
- 一度公開したあとも、ノードのメンテは必須です
- Node-RED、だぁいすき!
話の発端:Node-RED向けの京急ノードを作りたかった
この記事をご覧の諸兄姉にとっては釈迦に説法かもしれませんが、Node-REDはグラフィカルなUIで、ノンコーディングでもプログラミングができてしまう、素晴らしいツールでございます。
Node-REDで部品として動くパーツであるノードは色々準備されていたり、ノードライブラリでも種々のノードが公開されていて、Node-REDの可能性を無限に広げてくれています。
ただし、なかなか日本向けのノードがないのに玉に瑕で、だからこそ自分でノードを作って公開しようというモチベーションが湧いてきたわけです。とりわけ、私は京急が大好きなので、京急にまつわるノードを作ろうと思い立ったわけです。
で、作ったのがこれです。
node-red-contrib-keikyu
https://flows.nodered.org/node/node-red-contrib-keikyu京急の運行情報が取得できる、すばらしいノードに仕上がっています(笑)
問題①:なかなか公開できない!
詳しい経緯はこちらで公開していますが、公開作業をしている段階で一つの問題にぶち当たりました。
ノードがノードライブラリで公開されるまで、やることは色々あるわけですが、とりあえずコーディングやnpmjs.comでの公開までは順調に進んだわけです。
ただ、npmjs.comでnpmモジュールとしてノードを公開したあと、なかなかノードライブラリに反映されないという問題に陥りました。通常は数時間で反映されるわけなのですが、そのときは全く反映されませんでした。
よくある原因としては、
- package.jsonのkeywordsに「node-red」がない
- プレフィックス「node-red-contrib-」を用いて命名されてない
- README.mdがない
- LICENSEがない
- npmで公開されてない
- npm versionしたあとにgit pushし忘れてる
などなどありますが、それはすべてOK。結局、npmjs.comから一度ノードを削除して、再度公開しました・・・。削除後は24時間経たないと再公開できないという制限がありましたが、なんとか再公開後にノードライブラリに反映されました・・・。もし同じようなことで困っている方がいらっしゃいましたら、お試しくださいませ。
問題②:京急ノードが動かなくなった!
公開して、一安心してたんですが、ある日突然ノードが使えなくなっていました。
結論から言ってしまえば、京急の運行情報ページのレイアウトが更新されていて、いままで使っていたパースのロジックがワークしなくなり、運行情報の取得ができなくなっていました・・・。
もともとパースのロジックは、かなりのクソコードだったので致し方ないとおもいつつ、とりあえずコードを修正して、再度公開しました。
また、二度と同じようなことがないように、自分が作ったノードが正しく動作しているか、enebular上でCIのように定期的に動かし、ノードの状態を監視する仕組みを作りました。
こんな感じでステータスが表示されます。これで完璧ですね!(笑)
現在平常通り運転しています。
というわけで、今年もいろいろとお世話になりました。
来年もポキオと京急を何卒よろしくおねがいします!宣伝
ポキオとドライブをしながらIoTとかTechな話をする、ポキオ・カープール。
ぜひご覧ください!
一緒にドライブしながら喋ってくれる方も大募集中です!
- 投稿日:2019-12-15T02:32:46+09:00
Slackで匿名で投稿できるチャンネルを作ろうとしたら少しだけ苦労した話
はじめに
研究室でSlackを導入してから2年くらい経ちました。
話題でチャンネルを分けれるので非常に便利です。雑談用のチャンネルもあるのですが、特定の人ばかり話していて盛り上がりに欠けます。
「匿名ならみんな発言してくれるかも」と思ったのがきっかけで、匿名用のチャンネルを作りました。Googleで検索したら3年前のQiitaの以下の記事がヒットしました。
「超簡単にSlackで匿名の意見を投稿できるようにする」 @shibukk「超簡単」とありますが、記事通りにやっても上手くいかず、少しだけ苦労しました。
BotKitのバージョンが上がって中身が変わっていたのが原因でした。修正した部分を自分の備忘録としてまとめておきます。
実行環境はUbuntu16.04。
node.js, javascriptが動けばどこでも大丈夫なはずです。SlackのBotの取得
ここは本家と一緒です。
「Botを追加する」ここをクリックすると、現在ログインしているワークスペースでBotを作ることができます。
(「ワークスペースの管理者」じゃないと作れないかも・・・)今回はBotの名前を"anonymous_bot"にします。
API Tokenを後で使うのでコピーしておいてください。
漏れると悪用される恐れがあるので扱いには注意してください。(もしもの場合は再発行してください)チャンネルIDをの取得
ここから本家と手順が少し変わります。
以下のURLから匿名で会話したいチャンネルのIDを探してください。
https://slack.com/api/channels.list?token=さっき取得したAPI_Token
ちなみにチャンネル以外にも、特定のユーザーやプライベートチャンネルもできます。
詳しくは本家を参考にしてください。BotKitのインストール
次にBotKitをインストールします。
以下の手順に従ってください。//ディレクトリの作成 $ cd ~ $ mkdir slack_anonymous_bot $ cd slack_anonymous_bot //botkitをclone $ git clone https://github.com/howdyai/botkit.git $ cd botkit //branchを移動 $ git checkout origin/legacy //インストール $ npm install //もしインストールできなかったら・・・ $ npm audit fix $ npm install~/botkit/example/slack_bot.js が生成されていれば成功です。
プログラムの作成
以下の手順に従ってください。
//ディレクトリの移動 $ cd ~ $ cd slack_anonymous_bot/botkit/ //anonymous_bot.jsの作成 $ touch anonymous_bot.jsanonymous_bot.jsに以下のコードをコピペしてください。
anonymous_bot.jsvar Botkit = require("./lib/Botkit.js"); //パス注意 var os = require("os"); var controller = Botkit.slackbot({ debug: true, }); var bot = controller.spawn({ token: "先ほど取得したAPI_TOKEN" }).startRTM(); controller.on("direct_message", (bot, message) => { var now = new Date(); //時刻の取得 var user_name = "名無しさん: "+ now.getFullYear()+"/"+(now.getMonth()+1)+"/"+now.getDate()+"/ "+now.getHours()+":"+now.getMinutes()+":"+now.getSeconds(); bot.reply(message, "匿名で投稿しました."); bot.startConversation({ channel : "先ほど取得したチャンネルID" }, (err, convo) => { var send_message = { type: "message", channel: "先ほど取得したチャンネルID", text: message.text, username: user_name, thread_ts: null, reply_broadcast: null, parse: null, link_names: null, attachments: null, unfurl_links: null, unfurl_media: null, icon_url: null, icon_emoji: ":robot_face:", as_user: true } convo.say(send_message); }); });スクリプトの実行方法
//デバッグしたいときは $ cd ~/slack_anonymous_bot/botkit $ node anonymous_bot.js //通常時 $ cd ~/slack_anonymous_bot/botkit $ forever start slack_bot.js $ forever stop slack_bot.js ←止めたい場合実行結果
Botが起動している間は、Botの名前の横の○が緑色になります。
チャンネルIDを登録したチャンネルで匿名で投稿されます。(チャンネルのアプリにanonymous_botを追加するのをお忘れなく)
終わりに
無事に匿名で発言できるようになりました。
研究室のメンバーにも好評でした。
(若干チャンネルが荒れましたが・・・)
- 投稿日:2019-12-15T00:50:06+09:00
Vue.jsのコンポーネントのimport文をdynamic importに変換するcliコマンドを作りました
Vue.jsのコンポーネントのimportをdynamic importに変換するcliコマンドを作りました。
特定のディレクトリ配下のvueファイルを全てdynamic importに変換します。ソースはこちらで公開しています。
https://github.com/harhogefoo/dynamic-import-converter通常のcomponentのimport文<template> <div> <hoge /> <piyo /> </div> </template> <script> import Hoge from "@/components/Hoge.vue" import Piyo from "@/components/Piyo.vue" export default { components: { Hoge, Piyo } } </script>dynamic_importに変換<template> <div> <hoge /> <piyo /> </div> </template> <script> export default { components: { Hoge: () => import("@/components/Hoge.vue"), Piyo: () => import("@/components/Piyo.vue") } } </script>使い方
$ yarn global add dynamic-import-converter or $ npm install -g dynamic-import-converter $ dynamic-import-converter ./Vueファイルが格納されたディレクトリのパス/バグ、改善要望などは、リポジトリのissueまで!
https://github.com/harhogefoo/dynamic-import-converter/issues
- 投稿日:2019-12-15T00:43:25+09:00
Firebase Admin SDKで一般的なWebサービスの構成にFirebase Authenticationを使った認証処理を組み込む。
概要
Firebase #2 Advent Calendar 2019の14日目の記事です。
Firebase Authenticationを使用した記事は数多くあるのですが、
一般的なWebサービスのサーバーサイドでの認証にFirebase Authenticationを利用する
記事が見当たらなかったので、サンプル実装を書いてみました。今回のFirebase Authenticationの想定ユースケース
- 自前でのユーザー管理は行いたい
- 認証部分のみFirebase Authenticationを導入し、Googleアカウントとの紐付けを行いたい。認証処理の実装の省略したい。
システム構成図と認証フロー
今回作成するFirebase Authenticationを組み込んだWebアプリのシステム構成を図にしました。
以下のフローでFirebase AuthenticationによるセキュアなAPI実装を実現します。
- ① なんらかの認証トリガー(ログインボタンをクリックなど)から特定の認証プロバイダ(今回はGoogle)の認証画面へリダイレクトする。
- ②③ Firebase Authenticationが認証処理を行い、IDトークンが払い出される。
- ④ 何らかの認証が必要な処理を行うHTTPリクエストを送る。
- その際、
Authorization
ヘッダーに取得したIDトークンを付与してリクエストする。- ⑤ Firebase Admin SDKでIDトークンの検証を行う
- ⑥ するとユーザーIDやEmailアドレスなどが取得できるので、それを使って自前のUser Tableと突合し、正規のユーザーか判断・ユーザー情報の取得等ができるようになる。
サンプルアプリの仕様
今回作成するサンプルアプリの仕様です。
- ログインはGoogleの認証画面へのリダイレクトによって行う。
- ユーザーがその画面に来た時、そのユーザーがログイン済みであればそのユーザーの情報を画面に表示する。
- その情報は他の一般ユーザーが閲覧することはできない。
- ユーザーがログイン済みでなければ、ログイン画面(
/login
)に飛ばす。サンプルアプリでの採用言語+ライブラリ等
- クライアントサイド : TypeScript + Axios
- サーバーサイド : Node.js + TypeScript + Express
クライアントサイドでFirebase Authenticationによるログイン処理とAPIへのリクエストを行う
まずはクライアントサイドの実装からしていきましょう。
事前準備
Firebaseのプロジェクトを作成
先の方々に説明を譲ります。
SDKの取得
npmでFirebase SDKを取得します。
npm i firebase設定JSONの取得
Firebaseの設定をJSONで取得し、アプリケーションとFirebaseの紐付けを行います。
公式の記事を参考にしましょう。
https://firebase.google.com/docs/web/setup?hl=jaFirebase Hostingを使ってHTMLのホスティングを行う場合はこの設定JSONは必要ありませんが、ローカルでの確認する際など、あると便利なので一応取得することをオススメします。
実装
Firebase SDKを初期化
firebase.initializeApp
に先ほど取得した設定JSONを渡します。import firebase from "firebase/app"; import "firebase/auth"; const firebaseConfig = { /* 先ほど取得した設定JSONオブジェクト */ } firebase.initializeApp(firebaseConfig);ログイン処理を実装
リダイレクトによるログインを実装したい場合は以下
プロバイダをGoogleに指定する例です。const login = () => { const provider = new firebase.auth.GoogleAuthProvider(); firebase.auth().signInWithRedirect(provider); };ログイン自体の処理はこれで終わりです。あっけないですね。
これだけで実際にGoogleの認証画面へリダイレクトして、戻ってくる動きが確認できます。認証情報の取得
ログインした。という情報およびIDトークンを受け取ります。
認証情報はJSのロードから少し時間をおいて取得されるので、認証情報を使用する場合はそれが取得されたことをSubcribeする必要があります。
SubcribeにはonAuthStateChanged
メソッドを使用します。// firebase.auth().getRedirectResultはあまり使い勝手が良くなかった。 firebase.auth().onAuthStateChanged(async currentUser => { if (currentUser) { // ここでログインユーザーの情報が参照できる。 const { email, uid, displayName } = currentUser; console.log(email, displayName, uid); // IDトークンを取得する。 const idToken = await currentUser.getIdToken(); console.log(idToken); } else { /* 未ログイン時にはcurrentUserがnullで渡ってくるので nullチェックでfalseな分岐に未ログイン時の処理を記述する。 */ window.location.href = "/login"; } });
getRedirectResult
メソッドでもリダイレクト後の認証情報の取得は可能なのですが、それ以外のケース(すでにログイン済みのユーザーがページに訪れた時)などでの使い勝手が良くなかったで採用しませんでした。認証情報を使ってAPIにアクセスする。
サーバーサイドでのアクセスの検証に用いるため、IDトークンをHTTPヘッダーにつけてリクエストを飛ばします。
firebase.auth().onAuthStateChanged(async currentUser => { if (currentUser) { const idToken = await currentUser.getIdToken(); // 何らかの認証が必要なリクエストをIDトークン付きで飛ばす const res = await axios.get( BACKEND_SERVICE_BASE_URL + "/secret/userinfo", { headers: { Authorization: idToken } } ); console.log(res.data.user.secretData); } else { window.location.href = "/login"; } });クライアントサイドは以上です。
Firebase Admin SDKを使ってサーバーサイドでIDトークンを検証する。
さて、次はこのアクセスをサーバーサイドで検証するコードを実装してみましょう。
事前準備
SDKの取得
npmでFirebase Admin SDKを取得します。
npm i firebase-admin秘密鍵の生成
- Firebaseのコンソールから「プロジェクトの設定」 -> 「サービスアカウント」 を選択
- ページ下部にある「新しい秘密鍵の生成」を押下します。
- JSONがダウンロードできるのでそれを任意の場所に保存します。
きちんとした手順は公式を参照しましょう。(丸投げ)
https://firebase.google.com/docs/admin/setup?hl=ja実装
秘密鍵の適用とSDKの初期化
環境変数
FIREBASE_CONFIG
に先ほど保存したJSONのファイルパス、GOOGLE_CLOUD_PROJECT
にプロジェクト名を指定し、サーバーを起動させます。FIREBASE_CONFIG='path/to/secret.json' GOOGLE_CLOUD_PROJECT='projectname' node server.js #server.jsはコンパイル後のjsファイルSDKを初期化させる際はSDKが上記で設定した環境変数を勝手に見に行くので引数を渡す必要はありません。
import Admin from "firebase-admin"; const admin = Admin.initializeApp();APIでのIDトークンの検証
ExpressでAPIの処理を実装します。
verifyIdToken
メソッドを使い、クライアントから送られてきたAuthorization
ヘッダーのIDトークンを検証しuid
を取得します。uid
はユーザーごとに一意の値になるので、これをキーに別テーブルにてユーザ管理を行えば、Firebase Authenticationと自前のユーザー管理を紐づけることが可能です。const app = express(); // 諸々のミドルウェアの適用は省略 app.get("/secret/userinfo", async (req, res) => { const idToken = req.header("Authorization"); if (idToken) { const {uid} = await admin.auth().verifyIdToken(idToken); // uid を使って紐付けられたユーザー情報を取得する処理を行ったりする。 const someUseInfo = userService.getInfo(uid); res.json(someUseInfo); } // Authorizationヘッダーが無ければ403 res.status(403).send(); });以上です。
まとめ
簡単とは行かないまでも、小難しい認証処理をFirebaseに移譲できたのが良かったです。
今回は時間がなく準備できなかったのですが、後々サンプルコードを公開する予定ですので良くわからなかった方は参照ください。
- 投稿日:2019-12-15T00:29:06+09:00
macへnode.jsのインストール
macにnode.jsをインストールする時に、3記事くらい参考させていただいてやっとできたので、自分ができたやり方をメモしておく。
まず、インストールの手順としては以下になる。
1.homebrewのインストール
2.nodebrewのインストール(一瞬詰まった)
3.Node.jsのインストール(ここで結構詰まった)自分は、特に手順3の環境パスを通すところで詰まった。
1.homebrewのインストール
まずは、すでにインストールされているか確認。
$ brew -v -bash: /usr/local/bin/brew: No such file or directory上記のメッセージが表示されている場合は、インストールされていない。
インストール
https://brew.sh/index_ja.html に書いてあるスクリプトを実行する
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"2019/12/11現在は上記
homebrewがインストールできているか確認
$ brew -v Homebrew 2.2.1 Homebrew/homebrew-core (git revision 9ee2; last commit 2019-12-10)2.nodebrewのインストール
まずはインストールされているか確認
nodebrew -v -bash: /usr/local/bin/nodebrew: No such file or directory上記のメッセージが表示されている場合は、インストールされていない。
インストール
homebrew から nodebrew をインストールする
$ brew install nodebrewnodebrewがインストールできているか確認
$ nodebrew -v nodebrew 1.0.1上記のようにバージョンが表示されていれば、インストールに成功している。
3.Node.jsのインストール
インストールできるバージョンの確認
$ nodebrew ls-remotev0.0.1 v0.0.2 v0.0.3 v0.0.4 v0.0.5 v0.0.6 v0.1.0 v0.1.1 v0.1.2 v0.1.3 v0.1.4 v0.1.5 v0.1.6 v0.1.7 v0.1.8 v0.1.9 v0.1.10 v0.1.11 v0.1.12 v0.1.13 v0.1.14 v0.1.15 v0.1.16 v0.1.17 v0.1.18 v0.1.19 v0.1.20 v0.1.21 v0.1.22 v0.1.23 v0.1.24 v0.1.25 v0.1.26 v0.1.27 v0.1.28 v0.1.29 v0.1.30 v0.1.31 v0.1.32 v0.1.33 v0.1.90 v0.1.91 v0.1.92 v0.1.93 v0.1.94 v0.1.95 v0.1.96 v0.1.97 v0.1.98 v0.1.99 v0.1.100 v0.1.101 v0.1.102 v0.1.103 v0.1.104 v0.2.0 v0.2.1 v0.2.2 v0.2.3 v0.2.4 v0.2.5 v0.2.6 v0.3.0 v0.3.1 v0.3.2 v0.3.3 v0.3.4 v0.3.5 v0.3.6 v0.3.7 v0.3.8 (略・・・)上記コマンドで以下のようなエラーが発生する場合もあるそうです。(自分はならなかった)
取得:https://nodejs.org/dist/v7.10.0/node-v7.10.0-darwin-x64.tar.gz 警告:ファイルの作成に失敗しました 警告:/Users/whoami/.nodebrew/src/v7.10.0/node-v7.10.0-darwin-x64.ta 警告:r.gz:そのようなファイルまたはディレクトリはありません curl:(23)本文の書き込みに失敗しました(0!= 941) ダウンロードに失敗しました:https://nodejs.org/dist/v7.10.0/node-v7.10.0-darwin-x64.tar.gzその場合は以下のコードでディレクトリを作成すると良い。
$ mkdir -p〜/ .nodebrew / srcインストール
バージョンを指定してインストールするためには、以下のようにする。
$ nodebrew install-binary v10.16.3最新版をインストールしたい場合は以下を実行する。
$ nodebrew install-binary latestインストールされた Node.js のバージョンを確認する
$ nodebrew list v10.16.3 current: none使用する Node.js のバージョンを指定する
$ nodebrew use v10.16.3 use v10.16.3Node.js のパス設定
Node.js のバージョンを確認する。
$ node -v -bash: node: command not found※ 上記のようなメッセージが表示されていると、Node.js へのパスが通っていません
以下を実行する。
echo export PATH=$HOME/.nodebrew/current/bin:$PATH >> ~/.bash_profilenode から Node.js のバージョンを確認する(パスの疎通を確認する)
$ node -v v10.16.3※ 上記のように、バージョン情報が表示されれば Node.js にパスが通っています。
以上です!
大変お世話になったサイト
https://qiita.com/kyosuke5_20/items/c5f68fc9d89b84c0df09
あと2つ、なんて調べたか忘れてしまったので、また記載しておきます。
- 投稿日:2019-12-15T00:20:10+09:00
ObnizとAWS LambdaとLINEを使って家のエアコンをスマート化した話
きっかけ
僕の家は最寄駅から徒歩20分くらいありまして、その道を毎日歩いて通勤しています。春先や秋などは良いのですが、真夏日だと蒸し暑い中で家と会社を往復しています。
会社に行く時は電車に乗れさえすればエアコンが効いてて快適です!ただ帰りの場合は、家についても中は暑く、時には外よりも蒸し暑い時もあります。
どうせなら駅に着いた瞬間エアコンをつけて、駅から家まで歩いている間に涼しくなってくれればいいのに・・・作ったもの
LINEから「冷房」などとメッセージを送信すると、家のエアコンの冷房スイッチをONしてくれるbotを作成しました。
LINEなので当然、ローカルネットワークではなくインターネット回線越しです。なので家の外でも会社でもどこでも、家のエアコンをつけたり消したりできます!
これ作ったのほんとは夏頃なのですが、冬用に暖房ON機能も追加することで一年中活躍してくれそうです。システム構成・技術解説
システム構成はざっくりこんな感じです。
それぞれ詳細を書いていきます。LINE Developersでチャネル作成
LINEをBot化するにあたりLINE Developersにてチャネルを作成します。
せっかくbotにするので、嫁の好きなソシャゲのキャラにアイコンを設定。
(チャネル作成の詳しい手順については割愛します。)
「Messaging API設定」というタブの「Webhook URL」に、エンドポイントとなるAPIを設定する必要があります。今回は、API Gateway & AWS Lambdaで作成します。
また、チャネルアクセストークンとチャネルシークレット(こっちはチャネル基本設定タブの中にあります)も後々必要になるので控えておきます。エアコン(リモコン)の赤外線パターンを取得
秋月などで販売されてる、赤外線受信モジュールを使用します。
http://akizukidenshi.com/catalog/g/gI-00622/
これをObnizに直接、こんな感じで接続します。
あとは赤外線モジュールに向けてリモコン操作することで、その操作の赤外線点灯パターンを取得します。そのためのプログラムを書く必要があるのですが、Obniz公式が赤外線受信モジュール取り扱いのためのページを公開してくれており、そのページの中で、なんとプログラム実行ができてしまいます!
https://obniz.io/ja/sdk/parts/IRSensor/README.md
まずブラウザでObnizにログインしているかどうかをご確認ください。
ログインしているならば、「start(callback(array))」という欄に自身のobnizIDが表示されているはずです。そのボックス内のプログラムを、「TEST RUN」ボタン押下で実行することができます。
「TEST RUN」実行後、エアコンのリモコンを赤外線受信モジュールに向けて操作することで、赤外線点灯パターンの配列が表示されます。冷房・暖房をつけるパターンと停止するパターン、それぞれの配列パターンを取得・控えておきましょう。
ブレッドボードに基盤作成
まず下記のものを購入しましょう。
- 5mm 赤外線LED (http://akizukidenshi.com/catalog/g/gI-03261/)
- 光拡散用LEDキャップ (http://akizukidenshi.com/catalog/g/gI-01120/)
- 50Ω抵抗
- ジャンパワイヤx2本
- ブレッドボード
購入部品は全体的に下記記事を参考にさせていただきました。
https://qiita.com/KAKY/items/55e6c54fa2073cdc0bbe
上記部品たちを下記のように接続!
見えづらいかもですごめんなさい。
そこまで複雑な配線でもないですが、LEDの足が長い方(アノード:+)はObnizの0番(上記写真でいう赤いジャンパワイヤ)、足が短い方(カソード:-)はObnizの1番(上記写真でいう黒いジャンパワイヤ)に接続するように気をつけてください。
また当たり前ですが、その間に抵抗を挟んでおいてくださいね。(僕は開発中に一度抵抗を挟むのを失念し、LEDを一つ爆発させました。)
LEDキャップは、指向性のあるLEDの光を拡散させる役割で、これをつけておくとわざわざLEDをエアコンに向ける必要がなくなります。これも忘れずにつけておきましょう。Lambdaへのプログラムアップロード
上記のやり方でゲットした「冷房」「暖房」「停止」それぞれの赤外線パターン配列を控えて、Lambdaに記載するプログラムを書いていきます。
Lambda上でObniz APIをキックするために、またLINE Messaging APIを使用するために、それぞれNode.js用のSDKが必要になりますので、まずローカルPCにインストールしましょう。(僕はMacユーザーなので開発端末はMac想定でいきます。)
※ObnizのLambda連携は、下記公式のドキュメントもありますので、参考にしてみてください。
https://obniz.io/ja/lessons/server_side/lessons_lambda$ cd /path/to/the/work/dir/ $ npm install obniz $ npm install @line/bot-sdkそして同じ階層にindex.jsファイルを用意します。
index.js'use strict'; const line = require('@line/bot-sdk'); const crypto = require('crypto'); const client = new line.Client({channelAccessToken: process.env.ACCESSTOKEN}); const Obniz = require("obniz"); exports.handler = function (event, context) { var obniz = new Obniz("your-obnizID"); let signature = crypto.createHmac('sha256', process.env.CHANNELSECRET).update(event.body).digest('base64'); let checkHeader = (event.headers || {})['X-Line-Signature']; let body = JSON.parse(event.body); if (signature === checkHeader) { if (body.events[0].replyToken === '00000000000000000000000000000000') { //接続確認エラー回避 let lambdaResponse = { statusCode: 200, headers: { "X-Line-Status" : "OK"}, body: '{"result":"connect check"}' }; context.succeed(lambdaResponse); } else { obniz.onconnect = async function () { console.log('obniz'); let text = body.events[0].message.text; let res_text = 'うーん、私難しいことはわからないな〜'; if (text.match(/冷房/)) { res_text = 'わかった!冷房をつけておいてあげるわね。ひんやりひんやり〜'; }else if (text.match(/暖房/)) { res_text = 'わかった!暖房をつけておいてあげるわね。ぽっかぽかよ!'; }else if (text.match(/停止/)) { res_text = 'エアコン消しとくわね。節約節約!'; }else{ res_text = 'うーん、私難しいことはわからないな〜'; } var led = obniz.wired("InfraredLED", { anode:0, cathode:1 } ); obniz.display.clear(); obniz.display.print("Connected!"); console.log(res_text); if (text.match(/冷房/)) { console.log('reibo'); led.send([1,1,1,1,1, //...中略... 0,0,0,0,0]); obniz.display.clear(); obniz.display.print("Reibo ON"); }else if (text.match(/暖房/)) { console.log('danbo'); led.send([1,1,1,1,1, //...中略... 0,0,0,0,0]); obniz.display.clear(); obniz.display.print("Danbo ON"); }else if (text.match(/停止/)) { console.log('stop'); led.send([1,1,1,1,1, //...中略... 0,0,0,0,0]); obniz.display.clear(); obniz.display.print("OFF"); }else{ // } const message = { 'type': 'text', 'text': res_text }; console.log(message); client.replyMessage(body.events[0].replyToken, message) .then((response) => { let lambdaResponse = { statusCode: 200, headers: { "X-Line-Status" : "OK"}, body: '{"result":"completed"}' }; context.succeed(lambdaResponse); }).catch((err) => console.log(err)); await obniz.wait(5000); obniz.close(); } } }else{ console.log('署名認証エラー'); } };text.matchの部分は、obnizとの通信部分とMessaging APIとの通信部分のif文をまとめて書くと、なぜかObnizの動作が失敗するので、上記のように分けて書くことで動作させてます。もっといい書き方はある気がします・・・(JSわからない)
また、Lambdaの環境変数の中に、先ほど控えたチャネルアクセストークンとチャネルシークレットを記載するのを忘れずに。
Lambdaには本来コンソールから直接プログラムを記載できるのですが、ObnizやLINEのSDKごとアップロードしなければいけないので、zipに固めてアップロードします。
やり方はObniz公式のドキュメントに書いてあります。(さっきのと同じ)
https://obniz.io/ja/lessons/server_side/lessons_lambda
ただ、おそらくzip後のファイルが10MBを越えるため、一度S3バケットにアップロードしリンクURLを指定するやり方をとる必要があるかもしれません。Webhook設定
作成したLambdaとAPI Gatewayを紐付け、エンドポイントを作成します。
細かい説明は省きますが、メソッドはPOST、認証はNONEにしましょう。(認証挟んだ方がセキュアだと思いますが手抜きしてしまいました。誰かLINE-SDKとの連携方法のやり方教えてください・・・)作成できたAPIエンドポイントを、LINE DevelopersのWebhook URLに設定して、完成です!
まとめ
これで夏も冬も快適エアコンライフを過ごすことができそうです。おかげさまで嫁にも好評でよかったよかった!
今回は要件満たすためのミニマム実装だったので、赤外線パターンを直接ハードコーディングしたりしてますが、これもLINE上から登録できるようにして、DynamoDBあたりに保存するようにできるといいかもですね。
またせっかくbotにしたので、雑談的な会話も対応させてみたかったのですが・・・docomoの雑談対話APIが終了してしまったので断念。
無料で使える雑談APIどこかにないでしょうかね・・・