20191215のNode.jsに関する記事は20件です。

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を組み合わせた、簡単なプロキシを書きます。

slack-to-intranet.png

パターンとして変則的ではありますが、結果的に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の前段に置くプロキシシステムを検討し、その実装までをやってみました。ざっくり机上で検討 → ざっと実装した割には案外使えそうなものが考案できて、個人的には満足しています。

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

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.tsx
const 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.ts
import { 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.ts
import { 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.ts
const 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) // クライアントならクッキーをセット
}

まとめ

以上です。いかがでしたでしょうか?
参考になりましたら幸いです。

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

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.
d1fb5456080f4fbb895c367aae298593.jpeg
httpだと問題なく動きます。
create-react-appで作ったアプリを試しにHerokuに上げてみたときに、この問題を踏みました。

原因

以下でIssuesが上がっていました。
https://github.com/facebook/create-react-app/issues/8075
https://github.com/facebook/create-react-app/pull/8079

WebSocketsを利用している箇所で、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.0

react-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"
  }

2cd9968afd83f4f2863a9ff10f724a74.jpeg

上記Issuesは、react-scriptの3.3.1で修正予定(2019/12/15時点)のようなので、バージョン下げは暫定対応とし、修正されたら3.3.1に上げるのがよいと思います。

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

TwitterでTILしたらGitHubに草が生える

はじめに

TIL(Today I Learned): 今日学んだこと

をtwitterのつぶやきで行おうと思いました

  1. #til というハッシュタグで学びをつぶやく
  2. 1日1回自分の投稿の #til ハッシュタグを拾いに行く
  3. GitHubにコミット

というのがいけないかな〜と思ったのがきっかけです

AWSで CloudWatch Events + Lambda Functions あたりでできそうな気がしたのでやってみます

参考

準備

  • AWSアカウント
  • Twitter開発者アカウント
  • TILをコミットしていくGitHubリポジトリ

Twitter APIを利用するためには開発者アカウントの登録が必要です

※ 利用目的とか審査とかあってやや面倒です…

手順

til-twitter.png

  1. Amazon CloudWatch Eventsで定期的にAWS Lambdaを起動
  2. LambdaでTwitter APIを叩いて #til ツイートを取得
  3. #til ツイートがあればGitHub APIを叩いてコミットする

アプリケーションの動きとしては上記の流れを想定します

なのでやらないといけないことは、

  1. Lambda Functionsの作成
    1. Twitter APIを叩いて #til ツイートを取得するjsを作る
    2. GitHub APIを叩いてコミットするjsを作る
    3. Lambdaに登録する
  2. 定期的に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 として作成しておくことで解決できます

image-lambda-layers1.png

zipファイルを直接アップロードするか、 S3 に上げておいてそれを利用することができます

zipファイルの中身は注意が必要で、例えば modules.zip を解凍したときに以下の構成になっている必要があります

$ ls -1 modules/
node_modules/

node_modules の中に利用したいモジュールが入っているイメージです

この構成になっていないと、Lambda Functionsの方で利用するのにうまくいきません

先にLambda Layersを作ったら、Lambda Functionsを作成していきます

image-lambda1.png

ランタイムはLambda Layersと同じになるようにします

Lambda FunctionsもzipファイルをアップロードしたりS3のファイルを利用することができますが、今回はそのまま作成したコードを貼り付けます

image-lambda2.png

環境変数をたくさん使うので、ぽちぽち登録します

image-lambda3.png

タイムアウト設定をデフォルトの3秒 -> 30秒に変更しておきます

image-lambda4.png

最後にLambda FunctionsにLambda Layersを追加すればOKです

image-lambda5.png

右上から適当なテストイベントを作ってテストしてみて、無事に動けばLambdaとしては完成です

2. 定期的にLambda Functionsを実行するCloudWatch Eventsの作成

image-cloudwatch-events3.png

image-cloudwatch-events2.png

以上

簡単に設定できました

Cron式については、日付が変わったころに前日分のツイートを取得するような感じで実行されるようにします

CloudWatch EventsのCron式で実行されるイベントは、UTCで誤差1分以内だそうです

前日に #til ツイートをしていれば、日本時間でだいたい翌日の朝9時頃にコミットができあがる想定ですね

実行結果

image-tweet1.png

image-commit1.png

コミットされました

これで無事にツイートするだけで草が生える環境ができあがりました

学び

  • 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 FunctionsCloud Scheduler でも似たようなことができそうなのでやってみたい

まとめ

GitHubの草が生えているからといって活発に開発をしているとは限らないぞ

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

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.proto
syntax = "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.mod
module 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.go
package 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.proto
syntax = "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.js
const 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

無事データが送られました!
以上です!

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

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とハイフンがあっても処理してくれる。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

leapjs+johnny-fiveでクレーンゲームを操作する

はじめまして、@ufoo68です。普段はAWSとかReactを触る業務をやっておりますが、Qiitaでは色々と雑多なことを書いたりしております。

はじめに

今回は今更ながらLeap Motionを買ったのでこれをNode.jsのライブラリで遊んだりしておりました(何年前の記事だよと思われるかもしれませんが)。きっかけは私がとあるイベントでここ2年くらい前から展示している改造クレーンゲームがありまして↓

iOS の画像 (11).jpg

こいつは市販のやつArduino Nanoを仕込んだものになってます。Arduinoの中にはFirmataを書き込むことで、PC上で動くのプログラミング言語で操作することができます。今まではPythonとOpenCVを使って、手のオブジェクト検出を使って操作するものを展示していたのですが、どうも照明とか外の光加減とか手の形の個人差とかで認識精度が左右されて調整とかが難しかったのでここは思い切って、Leap Motionを買うことにしました。

まずは動画

以下が実際に動作したものになります。

IMAGE ALT TEXT HERE

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さんの投稿です。お楽しみに!

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

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側も意図していない動作をしてるように見受けられます。
(コピペミスとかでもなかったです)

2019-12-15-15-52-35.png

とはいえ、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と比較すると種類も増えてます。他にもあるかも。

リンク

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

金曜の夜になったら会社の 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] ボタンを押しましょう。

スクリーンショット 2019-12-15 1.14.03.png

APP 名を適当に入力し、どのワークスペースに作成するかを選択後、右下の [Create App] を押します。

スクリーンショット 2019-12-15 1.14.56.png

すると何かゴチャゴチャしたページに飛ぶので、Add features and functionality 節の右下辺りの [Permissions] を選択します。

スクリーンショット 2019-12-15 1.20.38.png

このページでどういう API の使用を許可するのかを設定します。

Scope 節の [Add an OAuth Scope] ボタンを押しましょう。
(大きい緑のボタンではなく下の白いボタンの方なので注意)

スクリーンショット 2019-12-15 1.23.14.png

今回利用するAPI の仕様によると、権限として dnd:write が必要と書いてあるので、検索し選択します。

スクリーンショット 2019-12-15 1.26.09.png

その後ページの一番上に戻ると、App がインストールできるようになっています。

スクリーンショット 2019-12-15 1.26.48.png

インストールに成功すると 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!] ボタンを押してください。

スクリーンショット 2019-12-15 1.46.04.png

まずはトリガーとなる App を選択してと言われるので、時刻をトリガーとする [Schedule by Zapier] を選択します。

スクリーンショット 2019-12-15 12.34.41.png

次に、どういう条件でトリガーさせるかを聞かれます。

トリガーさせたい曜日が1つだけの場合は every week, 複数ある場合は everyday を選択するといいです。

今回は説明のため everyday を選択します。

スクリーンショット 2019-12-15 12.36.43.png

上で everyday を選択するとトリガーさせる時刻を聞かれます。
金曜の午後9時以降は会社のことを忘れたいので「9pm」を選択し、CONTINUE します。

スクリーンショット 2019-12-15 12.38.50.png

次の画面で [TEST & CONTINUE] というボタンが出るので、押下してトリガーの設定を完了します。

次はアクションの設定です。下の [Do this...] をクリックしましょう。

スクリーンショット 2019-12-15 12.42.17.png

App は Code by Zapier を選択します。

スクリーンショット 2019-12-15 12.44.30.png

Node.js の ver 10.x.x を選択します。

スクリーンショット 2019-12-15 12.45.29.png

次に実行したいコードを記述していくのですが、その前に Zapier の Code のしくみをざっくり解説します。

Input Data

Code の設定項目には「Input Data」と「Code」があるのですが、まずはInput Dataから説明していきます。

スクリーンショット 2019-12-15 12.53.34.png

Input Dataでは、一つ前のトリガーやアクションからどういうデータをどのように受け取るかを設定します。

具体的にはここで「データのプロパティ名」と「データの種類」を入力しておくと、
コード内でinputData.プロパティ名の形でそのデータを取得できるようになります。

今回はスケジュールトリガーから曜日データを受けとりたいので、Pretty Day Of Week(整形された曜日情報)を、dayOfWeekのプロパティ名で受け取れるように設定しています。

Code

Code 節には実行するコードを書くのですが、少しクセがあって、

  • コード全体が async function にラップされている
  • オブジェクトまたはオブジェクトの配列をoutputという定義済み変数に入れなければならない

となっています。

outputに入ったものが次のアクションに渡されるしくみになっていて、
次のアクションがない場合でも必ず値を入れなければなりません。

また async function なのでawaitを使用することが可能です。
というか非同期の場合にawaitを使わないとoutputに何も入ってないよ!とエラーになる可能性があります。

実際のコードは以下のようになりました。

code
const 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] を押して次のような表示が出れば・・

スクリーンショット 2019-12-15 13.17.00.png

あとは作ったアプレットを ON にするだけで全て完了です!
お疲れ様でした。

補足

  • シンプルな要件のわりにはそれなりに実現が大変でした。公式で機能を作ってくれるといいですね。

  • 今回の方法だと週末以外の休日には対応できませんが、カレンダーと連携させるとより柔軟にミュートができるかもしれません。

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

Node.jsからSendGridを使ってメールを送る

久しぶりにSendGridを使ってみたのでメモ。

SendGridの注意点

  • ユーザ名はメールアドレスではなく、代理店である構造計画研究所から独自に振られたxxx@kke.comというやつ

久しぶりですっかりID忘れてました。

準備

API KEYの取得

利用するにはAPI KEYが必要です。到達率を上げるためにはドメイン認証やらいろいろやったほうがいい。
API KEYはSettingsの中にある。

スクリーンショット 2019-12-15 14.58.57.png

作業場の準備

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

届いたみたい。

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

Temporal dead zoneと死の秘宝

ChromeやNodeJSのJavascriptコンソール画面で動作確認する場合、
以下の様に間違ってエラーになってしまうことがあります。

const obj = JSON.parse(""); // JSON形式じゃない文字列を指定
// Uncaught SyntaxError: Unexpected end of JSON input

JSON形式の文字列で指定するところに空文字を指定した場合ですが、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 declared

MDNのlet変数やconst変数の説明を見てみるとTemporal dead zoneの存在について紹介されています。

undefined の値で始まる var 変数と異なり、 let 変数は定義が評価されるまで初期化されません。変数を宣言より前で参照することは ReferenceError を引き起こします。ブロックの始めから変数宣言が実行されるまで、変数は "temporal dead zone" の中にいるのです。

どうやら、宣言されたconstlet変数はTemporal dead zoneの中に残ってしまっていて参照できない状態になっているようです。

エラーが発生するなどして変数の初期化が行われなかった場合、letconstについてはTemporal dead zoneに変数があるため、宣言はされているが、参照出来ない状態となるらしいです。この状態だと変数を再使用することは出来なくなってしまいます。

対応策としては、
リロードなどをして最初から実行し直すか、
もしくは、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と死の秘宝)と呼んでいる記事があったのでタイトルも合わせて変えました。

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

Temporal dead zoneと消えない変数

ChromeやNodeJSのJavascriptコンソール画面で動作確認する場合、
以下の様に間違ってエラーになってしまうことがあります。

const obj = JSON.parse(""); // JSON形式じゃない文字列を指定
// Uncaught SyntaxError: Unexpected end of JSON input

JSON形式の文字列で指定するところに空文字を指定した場合ですが、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 declared

MDNのlet変数やconst変数の説明を見てみるとTemporal dead zoneの存在について紹介されています。

undefined の値で始まる var 変数と異なり、 let 変数は定義が評価されるまで初期化されません。変数を宣言より前で参照することは ReferenceError を引き起こします。ブロックの始めから変数宣言が実行されるまで、変数は "temporal dead zone" の中にいるのです。

どうやら、宣言されたconstlet変数はTemporal dead zoneの中に残ってしまっていて参照できない状態になっているようです。

エラーが発生するなどして変数の初期化が行われなかった場合、letconstについてはTemporal dead zoneに変数があるため、宣言はされているが、参照出来ない状態となるらしいです。この状態だと変数を再使用することは出来なくなってしまいます。

対応策としては、
リロードなどをして最初から実行し直すか、
もしくは、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と死の秘宝)と呼んでいる記事があった。

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

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 のGitHubページ

reveal.jsのセットアップには2つの方法があります。

1) 簡単に使うだけなら、githubのページからプロジェクトをローカルに保存するだけで大丈夫です。

2) 拡張マークダウンを使ったり、スピーカーノート(発表者のメモを隣に表示する機能)を使う場合には、Node.jsを使いローカルにサーバーを建てるFull Setupが必要です。

今回はこの(2)のFull Setupの方法を解説します。

1. reveal.js をクローン OR ダウンロードする。

reveal.js のGitHubページ

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-updates

ncuコマンドを使います

npm-check-updatesを実行するコマンド、ncuを実行します

$ ncu

するとncu -uをしてアップグレードをするように促されるので、仰せの通りにします。

$ ncu -u

7. 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を使うと非常に楽にスライドを作ることが出来るので、是非是非活用してください

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

Node.jsでのCLIの作り方と便利なライブラリまとめ

はじめに

Node.jsでCLI(Command Line Interface)を作りたくなることがあると思います。
そして、GitHubに公開されているCLIを見ると、色々なライブラリを組み組み合わせて便利なCLIを作っているようです。

この記事では、Node.jsでCLIをどう作るのか?そして、CLI開発を支える便利なライブラリを紹介します。

身の回りのCLI

CLIの作り方を見る前に、普段の開発で触れているCLIを見てみましょう。

ESLint

CLIには基本的に--helpオプションが用意されていますね。
スクリーンショット 2019-12-12 16.46.27.png

npm

ユーザーの入力を受け取る対話的なCLIも多いですね。
スクリーンショット 2019-12-12 16.45.29.png

expo

プレースホルダーがあることで入力する内容のイメージを伝えることができます。
スクリーンショット 2019-12-10 17.45.24.png

stencil

様々な選択方法をユーザーに提供したり、分かりやすく色付けすることも可能です。
スクリーンショット 2019-12-12 16.52.45 1.png
スクリーンショット 2019-12-12 16.54.16.png

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

また、readlinequestion関数を利用すると、ユーザーへのプロンプトメッセージの表示と、ユーザー入力の受け取りまでを行うことができ便利です。

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 -y

package.json bin

package.jsonbinフィールドで、コマンドとファイルのマッピングを行えます。

こうしておくことでパッケージのインストール時に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.jsonnamepublishConfigを指定します。

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.argvreadlineだと実装が複雑になったり面倒です。
また実際はコマンドライン引数のパースやオプション、バリデーション、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

https://github.com/cacjs/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 number

commander

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") + "!"));

スクリーンショット 2019-12-14 21.40.08.png

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を作成できるライブラリです。GatsbyParcelでも利用されているようです。

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のインターフェイスを作成できるライブラリです。回答の方法は入力、リストやチェックボックス、パスワード形式など、様々な方法が用意されています。

似たライブラリでEnquirerpromptsがあります。

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 3

oclif、gluegun

https://github.com/oclif/oclif
https://github.com/infinitered/gluegun

CLIを作成するフレームワークもあるようです。

まとめ

Node.jsでシンプルなCLIの作成方法から、CLI作成を簡単に行える便利なライブラリを紹介しました。

自分でも調べてみて、便利なライブラリや似たライブラリがとても多く、実際どれを使えばいいのか分かりませんでした。
だいたいできることは同じなので、サンプルコードを見て、好みで使ってみるのがいいと思います。

そして、安定のsindresorhusがとても便利なライブラリをたくさん作成してくれていることが分かります。

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

続・実録 Node-REDノード作成 24時

こんにちは、ポキオです。

IoTLT Advent Calendar 2019enebular Advent Calendar 2019の15日目の記事です。
手抜きです、ごめんなさい。

ポキオ Node-RED ノード作成

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

image.png

京急の運行情報が取得できる、すばらしいノードに仕上がっています(笑)

問題①:なかなか公開できない!

詳しい経緯はこちらで公開していますが、公開作業をしている段階で一つの問題にぶち当たりました。

image.png

ノードがノードライブラリで公開されるまで、やることは色々あるわけですが、とりあえずコーディングやnpmjs.comでの公開までは順調に進んだわけです。

image.png

ただ、npmjs.comでnpmモジュールとしてノードを公開したあと、なかなかノードライブラリに反映されないという問題に陥りました。通常は数時間で反映されるわけなのですが、そのときは全く反映されませんでした。

よくある原因としては、

  • package.jsonのkeywordsに「node-red」がない
  • プレフィックス「node-red-contrib-」を用いて命名されてない
  • README.mdがない
  • LICENSEがない
  • npmで公開されてない
  • npm versionしたあとにgit pushし忘れてる

などなどありますが、それはすべてOK。結局、npmjs.comから一度ノードを削除して、再度公開しました・・・。削除後は24時間経たないと再公開できないという制限がありましたが、なんとか再公開後にノードライブラリに反映されました・・・。もし同じようなことで困っている方がいらっしゃいましたら、お試しくださいませ。

問題②:京急ノードが動かなくなった!

公開して、一安心してたんですが、ある日突然ノードが使えなくなっていました。

image.png

結論から言ってしまえば、京急の運行情報ページのレイアウトが更新されていて、いままで使っていたパースのロジックがワークしなくなり、運行情報の取得ができなくなっていました・・・。

image.png

もともとパースのロジックは、かなりのクソコードだったので致し方ないとおもいつつ、とりあえずコードを修正して、再度公開しました。

image.png

また、二度と同じようなことがないように、自分が作ったノードが正しく動作しているか、enebular上でCIのように定期的に動かし、ノードの状態を監視する仕組みを作りました。

image.png

こんな感じでステータスが表示されます。これで完璧ですね!(笑)

現在平常通り運転しています。

というわけで、今年もいろいろとお世話になりました。
来年もポキオと京急を何卒よろしくおねがいします!

宣伝

ポキオとドライブをしながらIoTとかTechな話をする、ポキオ・カープール

image.png

ぜひご覧ください!
一緒にドライブしながら喋ってくれる方も大募集中です!

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

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.js

anonymous_bot.jsに以下のコードをコピペしてください。

anonymous_bot.js
var 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の名前の横の○が緑色になります。
Screenshot from 2019-12-15 02-18-28.png

anonymous_botにDMを送ると一瞬で返事が来て、
Screenshot from 2019-12-15 02-17-33.png

チャンネルIDを登録したチャンネルで匿名で投稿されます。(チャンネルのアプリにanonymous_botを追加するのをお忘れなく)
Screenshot from 2019-12-15 02-15-26.png

終わりに

無事に匿名で発言できるようになりました。
研究室のメンバーにも好評でした。
(若干チャンネルが荒れましたが・・・)

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

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

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

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アプリのシステム構成を図にしました。

認証-Page-2.jpg

以下のフローで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=ja

Firebase 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

秘密鍵の生成

  1. Firebaseのコンソールから「プロジェクトの設定」 -> 「サービスアカウント」 を選択
  2. ページ下部にある「新しい秘密鍵の生成」を押下します。
  3. 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に移譲できたのが良かったです。
今回は時間がなく準備できなかったのですが、後々サンプルコードを公開する予定ですので良くわからなかった方は参照ください。

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

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 nodebrew

nodebrewがインストールできているか確認

$ nodebrew -v
nodebrew 1.0.1

上記のようにバージョンが表示されていれば、インストールに成功している。

3.Node.jsのインストール

インストールできるバージョンの確認

$ nodebrew ls-remote
v0.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.3

Node.js のパス設定

Node.js のバージョンを確認する。

$ node -v
-bash: node: command not found

※ 上記のようなメッセージが表示されていると、Node.js へのパスが通っていません

以下を実行する。

echo export PATH=$HOME/.nodebrew/current/bin:$PATH >> ~/.bash_profile

node から Node.js のバージョンを確認する(パスの疎通を確認する)

$ node -v
v10.16.3

※ 上記のように、バージョン情報が表示されれば Node.js にパスが通っています。

以上です!

大変お世話になったサイト
https://qiita.com/kyosuke5_20/items/c5f68fc9d89b84c0df09
あと2つ、なんて調べたか忘れてしまったので、また記載しておきます。

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

ObnizとAWS LambdaとLINEを使って家のエアコンをスマート化した話

きっかけ

僕の家は最寄駅から徒歩20分くらいありまして、その道を毎日歩いて通勤しています。春先や秋などは良いのですが、真夏日だと蒸し暑い中で家と会社を往復しています。
会社に行く時は電車に乗れさえすればエアコンが効いてて快適です!ただ帰りの場合は、家についても中は暑く、時には外よりも蒸し暑い時もあります。
どうせなら駅に着いた瞬間エアコンをつけて、駅から家まで歩いている間に涼しくなってくれればいいのに・・・

作ったもの

LINEから「冷房」などとメッセージを送信すると、家のエアコンの冷房スイッチをONしてくれるbotを作成しました。

out.gif

LINEなので当然、ローカルネットワークではなくインターネット回線越しです。なので家の外でも会社でもどこでも、家のエアコンをつけたり消したりできます!
これ作ったのほんとは夏頃なのですが、冬用に暖房ON機能も追加することで一年中活躍してくれそうです。

LINEのトーク画面はこんな感じ。
カルヴィ.PNG
カルヴィ3.PNG
カルヴィ2.PNG

システム構成・技術解説

構成図.png

システム構成はざっくりこんな感じです。
それぞれ詳細を書いていきます。

LINE Developersでチャネル作成

LINEをBot化するにあたりLINE Developersにてチャネルを作成します。
LINEDEV1.png

せっかくbotにするので、嫁の好きなソシャゲのキャラにアイコンを設定。
(チャネル作成の詳しい手順については割愛します。)
「Messaging API設定」というタブの「Webhook URL」に、エンドポイントとなるAPIを設定する必要があります。今回は、API Gateway & AWS Lambdaで作成します。
また、チャネルアクセストークンとチャネルシークレット(こっちはチャネル基本設定タブの中にあります)も後々必要になるので控えておきます。

エアコン(リモコン)の赤外線パターンを取得

秋月などで販売されてる、赤外線受信モジュールを使用します。
http://akizukidenshi.com/catalog/g/gI-00622/
これをObnizに直接、こんな感じで接続します。
infrared_rec.JPG

あとは赤外線モジュールに向けてリモコン操作することで、その操作の赤外線点灯パターンを取得します。そのためのプログラムを書く必要があるのですが、Obniz公式が赤外線受信モジュール取り扱いのためのページを公開してくれており、そのページの中で、なんとプログラム実行ができてしまいます!
https://obniz.io/ja/sdk/parts/IRSensor/README.md
まずブラウザでObnizにログインしているかどうかをご確認ください。
ログインしているならば、「start(callback(array))」という欄に自身のobnizIDが表示されているはずです。そのボックス内のプログラムを、「TEST RUN」ボタン押下で実行することができます。
obniz_rec.png

「TEST RUN」実行後、エアコンのリモコンを赤外線受信モジュールに向けて操作することで、赤外線点灯パターンの配列が表示されます。冷房・暖房をつけるパターンと停止するパターン、それぞれの配列パターンを取得・控えておきましょう。

ブレッドボードに基盤作成

まず下記のものを購入しましょう。

購入部品は全体的に下記記事を参考にさせていただきました。
https://qiita.com/KAKY/items/55e6c54fa2073cdc0bbe
上記部品たちを下記のように接続!
基板.png
IMG_1623.JPG

見えづらいかもですごめんなさい。
そこまで複雑な配線でもないですが、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_env.png

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どこかにないでしょうかね・・・

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