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

文言のビルド失敗

$
0
0

はじめに

前回

コマンドラインから実行しました
READMEの通りに出来ず、そのレベルに達してないようですが、くじけず進めていきます

実行

usageによるとレンダリングが出来そうなので実行

node ./build/wenyan.js —render ./examples/helloworld.wy

エラー発生
/dev/stdoutにアクセス出来ません

windowsだからありません
リダイレクトしてみる

node ./build/wenyan.js —render ./examples/helloworld.wy > ./renders/helloworld.svg

エラー変わらず

残念

ビルド

それぞれのプラットフォーム用にビルドが必要なのかな。

Makefileを見て実行

cd tools
node ./make_cmdline.js

エラー発生
また/dev/~
デバイスがらみ。潰せない

エラーは出ていたが、wenyan.jsのタイムスタンプが変わったので、サイズが小さくなっているのがきになるが実行してみる。

レンダリングエラーメッセージ変わらず。
windowsでは、無理なのかな。

おわりに

またレンダリングに挑戦します


[Netlify Formsと連携]AWS Lambda上にGmail自動返信スクリプト(Node.js)をデプロイしAPI化。

$
0
0

Gatsby.jsを使用しNetlifyにデプロイするプロジェクトを進行していたところ、お問い合わせフォームの作成に手間取ったため備忘録。

前提

対象読者は、
・node.jsの使用経験はそこそこある。これは必須です。
・lambdaの存在と癖の強さをある程度把握してる。若しくは、どんな癖でもかかってこいという精神がある人。
・api gatewayとlambdaの連携を行ったことがある。若しくは、自力でどうにかできる自信がある人。
を満たしていれば、全員できると思います。

参考サイト等

詳しい。ここに従えば基本大丈夫(英語です。)
わかりやすい。コード以外はここに従えば大丈夫(日本語です。)

環境セットアップ

1.netlify formsをセットアップ(これが無くてもAPI化できるのですが、APIをテストする検証用という感じです。)

2.こちらにスクリプトを用意したので、ローカルに落とす(コードは下にも貼っておきます。)

3.上の参考サイトから、今回使用する、「clientId」と「clientSecret」と「refreshToken」をメモしといて、2でダウンロードしたスクリプトに記載。

4.ディレクトリ下でzip -r deploy.zip *を実行し、zip化(aws serverless frameworkを使用したい人は各々セットアップしてください)

5.一応lambdaのタイムアウト時間を10秒とかにしておきましょう。保存してください。

6.api gatewayで、lambdaをデプロイ。(後に詳しく説明します。一言で言うと、メソッドをPOSTに設定して、プロキシ統合させれば大丈夫です。)

7.netlify forms notificationにOutgoing webhookトリガーを設定。(後に軽く説明します。ご自身のnetlifyのベースURL/settings/forms#form-notificationsにて設定できます。)

8.以上で完成

という流れになります。3までは単純作業ですので、何か漏れがなければ大丈夫なはずです。5,6などはawsの使い方になりますが、経験がない方には負担の多い作業になると思うので後に説明します。7に関しては、ニッチな設定になるのですが、uiがわかりやすいので大丈夫だと思います。

6.api gatewayで、lambdaをデプロイ。について

僕は以下の構成で実現しています。
スクリーンショット 2019-12-22 19.24.01.png
メソッドの設定は、
スクリーンショット 2019-12-22 19.24.43.png
にて大丈夫です。
もちろん、Lambda関数は、ご自身で作られたlambda関数名を使用してください。

次に使うので、デプロイ先のURLはメモしておいてください。

7.netlify forms notificationにOutgoing webhookトリガーを設定。

まず、NetlifyのSetteingsのページに行ってください。そこの中にある、Formsをクリックしてください。
スクリーンショット 2019-12-22 19.27.12.png
そして、Form notificationsのAdd notificationをクリックして、Outgoing webhookを選択してください。
スクリーンショット 2019-12-22 19.29.30.png

コード

index.js
constnodemailer=require("nodemailer");const{google}=require("googleapis");constOAuth2=google.auth.OAuth2;constmymail="送信元のgmailアドレス";constclientId="OAuthのclientId";constclientSecret="OAuthのclientSecret";constrefreshToken="//developers.google.com/oauthplaygroundで取得したrefreshToken";exports.handler=async(event,context,callback)=>{//netlify formのqueryをparseして自動返信先のメールアドレスを取得.consttomail=JSON.parse(event.body).email;conststart=async()=>{constoauth2Client=newOAuth2(clientId,clientSecret,"https://developers.google.com/oauthplayground");oauth2Client.setCredentials({refresh_token:refreshToken});//アクセストークンに制限時間があるっぽい?ので毎度取得します。constaccessToken=awaitoauth2Client.getAccessToken()constsmtpTransport=nodemailer.createTransport({service:"gmail",host:'smtp.gmail.com',port:465,secure:true,auth:{type:"OAuth2",user:mymail,clientId:clientId,clientSecret:clientSecret,refreshToken:refreshToken,accessToken:accessToken}});constsendMail=async(mailingList)=>{constmailOptions={from:mymail,to:mailingList.join(', '),subject:"お問い合わせありがとうございます。",text:`${JSON.parse(event.body).email}様、お問い合わせありがとうございます。`};returnawaitnewPromise((resolve,reject)=>{smtpTransport.sendMail(mailOptions,(error,response)=>{error?console.log(error):console.log(response);smtpTransport.close();resolve();});});};awaitsendMail([tomail,mymail]);return;}awaitstart().then(()=>console.log('done'));//aws lambdaでは、responseの形式が決まっているので、適当に記述。constresponse={statusCode:200,headers:{"x-custom-header":"netlify forms test"},body:JSON.stringify(event)};returnresponse;};

まとめ

あまり時間がなく、シンプルなメモとなっていますが、補足リクエスト等あったらお願いします。時間ある際に追記しておきます。

迷惑メールを一掃するため,ドメインホワイトリストを作成した

$
0
0

みなさん,迷惑メールで苦労していませんか?

私は苦労しています.年末の大掃除ということで,迷惑メールを一掃してしまいましょう.

状況

キャリアメールに毎日,5〜10 通程度の迷惑メールが着弾します.有料の迷惑メールブロック機能を使用しているのですが,ブロックできるのは 5 割程度で,何通かは私のスマホの着信音を鳴らしてしまいます.

現在個人で利用しているメールアドレスは Gmail のもので,キャリアメールはほぼ使っていません.LINE や Twitter などの SNS 全盛の今(インスタ?TikTok?何それ,おいしいの?),使用するのは家族とのやり取りのみで,他には携帯メールのアドレスでなければ登録できないサービスや,かなり昔に登録したサービスのものがたまに届く程度です.

もはや,受信するメールのほとんどが迷惑メールという状況です.1 年前くらいまではほどんど迷惑メールなんてこなかったのに,なぜほとんど使わないアドレスに急に届くようになったのでしょう.どこかが個人情報流出させたのでしょうか?

対応方針

使っていないメールアドレスとは言っても,全く使っていないわけではないですし,古い知り合いはこのメールアドレスしか知らないということもあります.本来ならメールアドレスを変更するのが筋でしょうが,今では疎遠になっている人たちにアドレス変更を連絡するのも億劫です.何より,アドレス変更したところでどうせまた迷惑メールは届くようになるわけですから,「たとえ迷惑メール業者にメールアドレスを知られても,迷惑メールは届かない」ようにしたいです.

迷惑メールの傾向を調べてみると,適当な文字列で取得したと思われるドメインから送信されています.送信元を偽装しているメールは受信しないように設定しているので,届いているメールに関しては送信元の偽装は行われていません.また,タイトルや本文は,芸能人を思わせるような文脈であったり,何かのサイトの料金を請求するもの,ただ何かの URL が書いてあるだけのものなど,さまざまです.一貫している特徴としては,日本語がおかしいことくらいでしょうか?

ちなみに,「鈴木さんですよね?〇〇(姓)さんから聞きました」という,疎遠になっている知人を装うメールもありました.馬鹿なのかな?私が〇〇だ.

迷惑メールであることを的確に判断するポイントはなさそうですが,正規のドメインを乗っ取るようなことまではしていないので,ホワイトリストで受信するドメインを指定するようにします.やり取りする相手が限られていることと,今後も増えることはほぼないと考えられることから,この対応で少なくとも今は問題ないでしょう.

ホワイトリスト作成

幸運なことに,私はこれまで受信したメールは削除せず,用が済んだメールは「アーカイブ」という名前のフォルダに移動するという使い方をしています.また,迷惑メールは即迷惑メールとして報告する(報告後,ゴミ箱に移動される)か「迷惑メール」というフォルダに放り込むようにしていたため,本来のメールと迷惑メールは完全に分離されています.

このため,「アーカイブ」フォルダから送信元メールアドレスを抽出し,ドメイン部分を取り出すだけでホワイトリストを作れそうです.

いつの間にかキャリアメールも IMAP でアクセスできるのが当たり前になっていますね.NPM の imapというパッケージを使い,指定したフォルダから送信元ドメインを抽出する簡単なスクリプトを書いてみます.

最初に imapと,TypeScript で書きたいので ts-node, typescript, @types/imapをインストールします.

$ yarn add imap
$ yarn add -D ts-node typescript @types/imap

tsconfig.jsonを書きます.

tsconfig.json
{"compilerOptions":{"target":"es5","module":"commonjs","moduleResolution":"node","allowSyntheticDefaultImports":true,"esModuleInterop":true},"exclude":["node_modules"]}

スクリプト本体である main.tsを書きます.書き捨てのコードなのでちゃんとしたものではないですが,対象のフォルダは決めうちにせず,コマンドライン引数で対象のフォルダを指定するようにします.

main.ts
importutilfrom'util';importImapfrom'imap';constimap=newImap({host:process.env.IMAP_HOST||'',port:parseInt(process.env.IMAP_PORT||'993',10),user:process.env.IMAP_USERNAME||'',password:process.env.IMAP_PASSWORD||'',tls:(process.env.IMAP_TLS||'1')==='1'});// util.promisify を使うと,コールバックをとる非同期関数を// Promise を返す関数に変換することができます.constopenBox=util.promisify(imap.openBox.bind(imap));constgetBody=(message:Imap.ImapMessage)=>newPromise<string>((resolve,reject)=>{letbuffer='';message.on('body',stream=>{stream.on('data',(chunk:Buffer)=>(buffer+=chunk.toString('utf8'))).on('error',reject);}).on('end',()=>resolve(buffer));});imap.once('ready',async()=>{constdomains=newSet();constname=process.argv[2];// openBox でフォルダを開きます.// 第 2 引数が true なら Read Only モードになります.// 今回は読み込みしか行わないため,安全のために true にしておきます.constbox=awaitopenBox(name,true);imap.seq.fetch(`1:${box.messages.total}`,{// bodies には取得する要素を指定するようです.// 欲しいのは送信元だけなので,ヘッダーの FROM だけを指定します.bodies:'HEADER.FIELDS (FROM)'}).on('message',asyncmessage=>{constbody=awaitgetBody(message);constheaders=Imap.parseHeader(body);// ドメイン部分だけを取り出し,標準出力に表示します.headers.from.map(x=>x.replace(/.*<(.*)>$/,'$1')).map(x=>x.split('@').pop()).forEach(domain=>{// 一度表示したドメインは domains に格納し,// 2 回以上表示しないようにします.if(domains.has(domain))return;domains.add(domain);console.info(domain);});}).on('error',err=>console.error(err));imap.end();}).connect();

各種環境変数を設定します.私は direnvを使っているので,パスワード以外は .envrcに環境変数を設定します(使用していない場合は,コマンド実行の前に設定すればいいです).パスワードはどこにも残したくないし,画面にも一切表示されないようにしたいので,いつも read -sで入力するようにしています.

.envrc
layout node

export IMAP_USERNAME=<ユーザー名>
export IMAP_HOST=<IMAP サーバー>
$echo-n'Password: '>&2;read-s p;echo>&2;Password:
$IMAP_PASSWORD=$p ts-node main.ts アーカイブ | tee domains.txt
mail.yahoo.co.jp
ezweb.ne.jp
accounts.google.com
k.vodafone.ne.jp
...

これでドメインのリストが得られました.

ここで念のため,生成されたリストが正しいことを確認しておきましょう.私の場合,1 件だけ迷惑メールが「アーカイブ」に紛れ込んでいたため,見覚えのないドメインがありました.

ホワイトリスト設定

今回生成されたホワイトリストには 29 件のドメインがありました.もっと多かったらスクリプト書こうと思ってましたが,この程度なら手動入力でいいでしょう.

ここで一つ注意点があります.私の使用しているキャリアは docomo なのですが,ホワイトリストの設定は,実はドメインではなく,送信元メールアドレスとの後方一致で判定されると書かれています.このまま生成されたドメインを設定すると,「spammixi.com」のようなドメインからのメールを受信してしまいます.これを防ぐためには「@spammixi.com」というように「@」から書いておくとよいです.

image.png
https://www.mydocomo.com/dcm/dfw/web/pub2/useful/sp_mode/domain/spam.html

docomo には上のように「@mixi.jp」を許可し,「mixi.jp」を拒否すると書かれていますが,今回は PC メール全てを拒否にしているので,許可だけを指定します.

入力時に足すこともできますが,面倒なので awkで加工します.

$cat domains.txt | awk'$0 = "@" $0'@mail.yahoo.co.jp
@ezweb.ne.jp
@accounts.google.com
@k.vodafone.ne.jp
...

これをひとつひとつコピペして設定していけば,対応完了です.

さらば,迷惑メール.

後始末

もう「迷惑メール」に放り込んでいたメールは不要なので,削除しました.また,有料の迷惑メールブロック機能も今後は無用の長物となってしまうので,少し様子を見て解約しようと思います.

昔話

そういえば大学入学して間もない頃,引っ越して 1, 2 週間くらいで,ある国連機関と同名の謎の組織からダイレクトメールが届いたことがありました.その時点で住所知ってるのって大学を除くと不動産業者,電力会社,ガス会社,水道局だけです.契約直後の個人情報売ったら足がつくのなんて誰でもわかりますし,一体どこが売ったんですかね?

toioで生き物っぽい何かを作ってみた

$
0
0

ご覧いただきありがとうございます。
この記事はSB-AI Advent Calendar 2019の18日目の記事となります。

はじめに

突然ですが皆様は「人工生命」と聞いてどんなものをイメージしますか?
SFに出てくる怪物のようなイメージを持つ方も多いかもしれませんが、家を掃除してくれるルンバも人工生命の一つと言えます。
「人工生命」は国際的には「ALIFE」という名称で世界各国で研究が進められており、人工的に生命を作る事を通じて生命とは何かを理解しようとする研究分野の事です。

私はこの分野の研究者でもなんでもない、ただのサラリーマンですが、仕事でロボットに関わる機会が多く、
また昨年ALIFE関連のハッカソンに参加しチームで作品を作った経験から、プライベートで「人工生命」を1つのテーマとしてモノづくりをしております。
(嬉しい事にチームで作った作品が、今年の文化庁メディア芸術祭のアート部門で審査委員会推薦作品として選出されました!動画はこちら

今回の記事は、私が最近注目している「toio(トイオ)」というロボットのあるアプリに着目し、それを参考にtoioで生き物っぽい何かを作ってみた。という記事です。
※この記事は筆者がtoioで遊んでみて勝手に思った事を書いているだけの記事となりますので、ALIFEの研究記事等ではございません。ご了承ください。

toioについて

toioはsonyから2019年3月に発売されたロボットトイです。
https___toio.io_images_body_cover.jpg
※画像はこちらより引用:

公式サイトを見ていただけると一目瞭然ですが、主に子供向けのプログラミングトイとしての用途が主で、
カセットを用いて遊べる内容を変えたり、白いキューブにレゴなどを用いてオリジナルな何かを作ったりできるようになっています。
またスクラッチベースのビジュアルプログラミング環境の提供もされています。

画像にある白いキューブが「toio core cube」と言われるロボット本体なのですが、このcubeの仕様はネット上に公開されており、
またtoio.jsというnode.js用のライブラリも合わせて提供されていたり等、子供から上級者な大人まで遊べるようになっています。

今後も新たなエンタメ系ソフトやアプリの登場が予定されており、今後の展開が楽しみなtoioですが、
個人的に気になったtoio用スマホアプリが11月から提供開始となりました。

「ウロチョロス」現る

11月12日にtoioの新プロジェクトの発表があり、その際に発表された一つに「ウロチョロス」というアプリがありました。
(開発元のモリカトロンさんの記事より引用:https://morikatron.ai/2019/11/toio_urochoros/)

当時はtoioを買おうか迷っていたものの、TLに流れてきた下記のツイートの動画たちを見てすぐにamazonで購入しました。
生き物っぽいものを作りたい私にとって、まさに目指すべき一例でしたので即決でした。

動画では鬼ごっこをしていますが、この他にも自由にcube達に動き回ってもらうモードや、ユーザーの表情判定をcube達が行うモードがあったりします。
(※顔の撮影はスマホで行う)
なお、ユーザーの表情判定は純粋に画像認識したときのスコアで順位をつけているのではなく、最終的に個々のcubeが持つ評価基準を用いて判定が下されるのだとか。

ウロチョロスの生き物らしさとは

自分も実際にウロチョロスを試してみて、自由に動き回るモードでは様々なcube達のやり取りが観察できました。
・会話をしているようなアクション
・モノマネ(一方のcubeが他方のcubeに技を教える)
・パワー切れのcubeに対してのサポートアクション
・ダンス 等。

そこでわかってきたのは、ウロチョロスはcube同士の相互作用によって、生き物らしく見せているのではないか、という点でした。
コードは確認できないため、あくまでも想像ではありますが、一方のアクションが他方のアクションに影響を及ぼすような制御が多くなされていると見受けられます。
ここで考えたいのは、双方のアクションを把握するためには、toioがどこにいるかなど、周辺環境の情報取得の方法です。
昨今のロボットはカメラやセンサーによって周辺環境位を把握し自己位置推定等を行いますが、toioには見た目の通りカメラなどはありません。
そこでポイントになってくるのが「プレイマット」です。

toioのプレイマットについて

id_position_id_coordinate.png
※画像はこちらより引用
ウロチョロスで遊ぶには、「トイオコレクション」と言う別売のキットに付属する「プレイマット」と呼ばれる台紙が必要となります。
この台紙はただの台紙ではなく、紙面上に座標を示すPositionIDが印刷されている特殊な台紙となっています。
cubeにはそのPositionIDを読み出すセンサーが搭載されており、そのおかげでcubeの現在のx,y座標、さらにはcubeのx軸に対しての時計回りの角度を利用したプログラミングをすることが可能です。

つまり、このプレイマット上であればtoioを自由に操ることが可能であり、次からは2台のcubeの座標データを用いてそれっぽいものを作ってみたいと思います。

生き物っぽい何かを作ってみる

■実行環境
・macOS Catalina 10.15.2
・Node.js v12.13.0
・toio.js 42ae66a

細かいセットアップ及び関数の説明については省略いたします。
下記が今回作成したコードとなります。toio.jsのサンプルをベースとしています。

random_chase.js
const{NearScanner}=require('@toio/scanner')// calculate chasing cube's motor speedfunctionchase(jerryX,jerryY,tomX,tomY,tomAngle){constdiffX=jerryX-tomXconstdiffY=jerryY-tomYconstdistance=Math.sqrt(diffX*diffX+diffY*diffY)if(distance<70){return[0,0]// stop}letrelAngle=(Math.atan2(diffY,diffX)*180)/Math.PI-tomAnglerelAngle=relAngle%360if(relAngle<-180){relAngle+=360}elseif(relAngle>180){relAngle-=360}constratio=1-Math.abs(relAngle)/90letspeed=80if(relAngle>0){return[speed,speed*ratio]}else{return[speed*ratio,speed]}}asyncfunctionmain(){// start a scanner to find nearest two cubesconstcubes=awaitnewNearScanner(2).start()// connect two cubes (tom chases jerry)constjerry=awaitcubes[0].connect()consttom=awaitcubes[1].connect()//set movable areaconstmin_x=100constmax_x=400constmin_y=100constmax_y=400// set light color and store positionletjerryX=0letjerryY=0letjerryAngle=0jerry.turnOnLight({durationMs:0,red:255,green:0,blue:255})jerry.on('id:position-id',data=>{jerryX=data.xjerryY=data.yjerryAngle=data.angle})// set light color and store positionlettomX=0lettomY=0lettomAngle=0tom.turnOnLight({durationMs:0,red:0,green:255,blue:255})tom.on('id:position-id',data=>{tomX=data.xtomY=data.ytomAngle=data.angle})constrandRange=(min,max)=>Math.floor(Math.random()*(max-min)+min);varrandom_x=100varrandom_y=100setInterval(()=>{random_x=randRange(min_x,max_x)random_y=randRange(min_y,max_y)},2000)setInterval(()=>{jerry.move(...chase(random_x,random_y,jerryX,jerryY,jerryAngle),100)console.log("jerryX : "+random_x+" jerryY : "+random_y+" jerryAngle"+jerryAngle+"\n");},50)setInterval(()=>{tom.move(...chase(jerryX,jerryY,tomX,tomY,tomAngle),100)},100)}main()

これで動かしてみるとこのような動きになります。

X,Y座標を指定範囲内でランダムに一定時間で変化させ、cube1(jerry)がその座標を追いかけて、cube2(tom)がcube1を追いかける、といった内容です。

ただ、この状態だとcube同士がよくぶつかるため、cube内のモーションセンサで衝突検知して距離を取るなどしてあげないといけないのですが、そちらの実装はまた次回にしたいと思います。。
それができてやっと相互作用について少し語れるようになるかと思うのですが、時間の都合上今回はここまでといたします。

最後に

今回はウロチョロスアプリの影響を受け、toioで生き物っぽい(定義が曖昧で申し訳ありません、、)何かを作ってみました。
本当はもう少しALIFEと絡めることができれば良かったのですが、完全に力不足です。。修行あるのみですね。。

toioのcubeとプレイマットの組み合わせは、まだまだ面白そうなものが作れそうな予感がします。
マット上であれば座標だけでなく角度も取得できるので、cubeを出力デバイスではなく、入力デバイスとして使ってみるのも面白そうです。
(個人的にはTouchDesignerのコントローラーにしてみたいと考えています。)

以上、ありがとうございました。

Node.js で stream を使って gzip ファイル全体をメモリに乗せずに先頭一行だけを取得する

$
0
0

この記事は Node.js アドベントカレンダー 2019の 23 日目です。

はじめに

gzip ファイルなどの圧縮されたファイルを読み込む際、たとえば csv など圧縮率の高いファイル形式かつ大きなファイルの場合、全てをメモリに乗せ切れないことが稀にあります。
そもそも csv のカラムだけ欲しいなどの場合にデータ全体を取得するのは時間もかかるし無駄です。
そこで、 Node.js は stream を扱いやすい言語なので、これを使って簡単に解決できるため紹介します。

なお、 S3 からのデータ取得であっても createReadStream()すれば stream.Readable型になるため、同様の手法が可能です。そもそも、この話自体がローカルよりはクラウド絡みの方が多いケースになると思いますが……。

おさらい: Node.js での stream でのファイル読み書き

例えば、ファイルを読み込んで標準出力に表示します。

importfsfrom'fs';constinput=fs.createReadStream('tsconfig.json','utf-8');input.pipe(process.stdout)

書き込みの例として、大きなサイズの csv を生成するスクリプトをファイルに書き込みます。

src/create-big-csv.ts
importfsfrom'fs';constout=fs.createWriteStream('bigdata.csv','utf-8');constarr=[...Array(100000)].map((_,idx)=>idx);out.write("id,pow\n");arr.forEach(idx=>{out.write(`${idx}, ${idx*idx}\n`);})

大きなデータなので複数に別れて buffer で流れます。データが来るたびに区切り文字を表示して標準出力に表示する場合はこうです。

importfsfrom'fs';constinput=fs.createReadStream('bigdata.csv','utf-8');input.on('data',(buf)=>{console.log(buf.toString())console.log('---')});

gzip の展開を stream に適用する

標準ライブラリの zlib から pipe を作成し適用します。

importzlibfrom'zlib';importfsfrom"fs";constgzip=zlib.createGunzip()asyncfunctionmain(){constreadStream=fs.createReadStream('bigdata.csv.gzip')readStream.pipe(gzip).on('data',(buf)=>{console.log(buf.toString())console.log('---')})}main().catch((e)=>{console.error(e);process.exit(1);});

先頭一行を取得する

普通に buf.toString()した値を "\n"で split できます。
なお、 stream.destroy()が間に合わず次のデータが流れてくることは普通にあるので、一度限りの処理に限定できるよう関数に切り出すのが良さそうです。

importzlibfrom'zlib';importfsfrom"fs";constgzip=zlib.createGunzip()asyncfunctionmain(){constreadStream=fs.createReadStream('bigdata.csv.gzip')constfirstLine=awaitgetFirstLineFromStream(readStream.pipe(gzip))console.log(firstLine)}asyncfunctiongetFirstLineFromStream(stream:Readable){returnnewPromise((resolve,reject)=>{stream.on('data',(buf)=>{stream.destroy();conststring=buf.toString();const[firstLine]=string.split("\n");resolve(firstLine);})stream.on('error',reject);})}main().catch((e)=>{console.error(e);process.exit(1);});

おまけ: S3 から取得した gzip の先頭一行を取得する

s3.getObject().createReadStream()するだけです。 await は要りません。

importzlibfrom'zlib';importfsfrom"fs";import{S3}from'aws-sdk';constgzip=zlib.createGunzip()asyncfunctionmain(){consts3=newS3()constreadStream=s3.getObject({Bucket:'your-awesome-bucket',Key:'bigdata.csv.gzip'}).createReadStream();constfirstLine=awaitgetFirstLineFromStream(readStream.pipe(gzip))console.log(firstLine)}asyncfunctiongetFirstLineFromStream(stream:Readable){returnnewPromise((resolve,reject)=>{stream.on('data',(buf)=>{stream.destroy();conststring=buf.toString();const[firstLine]=string.split("\n");resolve(firstLine);})stream.on('error',reject);})}main().catch((e)=>{console.error(e);process.exit(1);});

おわりに

大きなデータを扱うときは、メモリに乗り切らないこともあるので stream を使いましょう。

Maceyarn installしたらnode-gypのエラーが出た

$
0
0

どうした

既存のプロジェクトの手直しをしようとして、ローカルにnode_modulesを入れたかったので以下のコマンドを実行したらエラーが出た。

yarn
//yarn install の短縮形

エラーは以下。

Command: node-gyp rebuild
Arguments: 
Directory: /Users/user/.ghq/github.com/hppRC/gatsby-firebase-sample/node_modules/fsevents
Output:
gyp info it worked if it ends with ok
gyp info using node-gyp@5.0.5
gyp info using node@13.2.0 | darwin | x64
gyp info find Python using Python version 2.7.16 found at \"/System/Library/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python\"
gyp info spawn /System/Library/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python
gyp info spawn args [
gyp info spawn args   '/usr/local/Cellar/node/13.2.0/libexec/lib/node_modules/npm/node_modules/node-gyp/gyp/gyp_main.py',
gyp info spawn args   'binding.gyp',
gyp info spawn args   '-f',
gyp info spawn args   'make',
gyp info spawn args   '-I',
gyp info spawn args   '/Users/user/.ghq/github.com/hppRC/gatsby-firebase-sample/node_modules/fsevents/build/config.gypi',
gyp info spawn args   '-I',
gyp info spawn args   '/usr/local/Cellar/node/13.2.0/libexec/lib/node_modules/npm/node_modules/node-gyp/addon.gypi',
gyp info spawn args   '-I',
gyp info spawn args   '/Users/user/Library/Caches/node-gyp/13.2.0/include/node/common.gypi',
gyp info spawn args   '-Dlibrary=shared_library',
gyp info spawn args   '-Dvisibility=default',
gyp info spawn args   '-Dnode_root_dir=/Users/user/Library/Caches/node-gyp/13.2.0',
gyp info spawn args   '-Dnode_gyp_dir=/usr/local/Cellar/node/13.2.0/libexec/lib/node_modules/npm/node_modules/node-gyp',
gyp info spawn args   '-Dnode_lib_file=/Users/user/Library/Caches/node-gyp/13.2.0/<(target_arch)/node.lib',
gyp info spawn args   '-Dmodule_root_dir=/Users/user/.ghq/github.com/hppRC/gatsby-firebase-sample/node_modules/fsevents',
gyp info spawn args   '-Dnode_engine=v8',
gyp info spawn args   '--depth=.',
gyp info spawn args   '--no-parallel',
gyp info spawn args   '--generator-output',
gyp info spawn args   'build',
gyp info spawn args   '-Goutput_dir=.'
gyp info spawn args ]
No receipt for 'com.apple.pkg.CLTools_Executables' found at '/'.

No receipt for 'com.apple.pkg.DeveloperToolsCLILeo' found at '/'.

No receipt for 'com.apple.pkg.DeveloperToolsCLI' found at '/'.

gyp: No Xcode or CLT version detected!
gyp ERR! configure error 
gyp ERR! stack Error: `gyp` failed with exit code: 1
gyp ERR! stack     at ChildProcess.onCpExit (/usr/local/Cellar/node/13.2.0/libexec/lib/node_modules/npm/node_modules/node-gyp/lib/configure.js:351:16)
gyp ERR! stack     at ChildProcess.emit (events.js:210:5)
gyp ERR! stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:270:12)
gyp ERR! System Darwin 19.2.0
gyp ERR! command \"/usr/local/Cellar/node/13.2.0/bin/node\" \"/usr/local/Cellar/node/13.2.0/libexec/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js\" \"rebuild\"
gyp ERR! cwd /Users/user/.ghq/github.com/hppRC/gatsby-firebase-sample/node_modules/fsevents

前までは普通にできとったやんけ!と思いつつ解決法を探して彷徨ったので記録しておく。

環境

  • ProductName: Mac OS X
  • ProductVersion: 10.15.2
  • BuildVersion: 19C57
  • node: 13.5.0
  • npm: 6.13.4

It works for me!

まずこのコマンドを実行して、何も出力がないことを確認して欲しい。

pkgutil --packages | grep CL

出力が存在しないことを確認したら、ターミナルで以下のコマンドを実行して、MacのCommand Line toolsを再インストールしたところ、エラーが解決したことが確認できた。

yarn cache clean
sudo rm -rf $(xcode-select -print-path)
xcode-select --install

xcodeを入れ直すと解決するようだ。

もしこの方法で解決しなければ、このGitHubのIssueが参考になるので漁って見て欲しい。

まとめ

もっと親切なメッセージを出してくれ頼む(たのむ)

node/js向け husky使ってpush前にテストするを共有する

$
0
0

テストや静的コード解析実行前にうっかりpushしてしまう事はないでしょうか?

有意義レビューの為にも事前にテストを実行し、リモートにはテスト通ったコードしか置かないようにしたいですね。

githooksのpre-pushを利用したpush前に必ずテストし、失敗したコードはpushできない状態を作る事ができます。

githooksのpre-pushを利用したpush前テスト

.git/hooks/pre-pushに実行したいコマンドを書きます。 pre-pushgit pushの前に実行されるフックです。

ほとんどのテストコマンドは、すべて通った場合に終了コード 0を、失敗した場合に 1などの0以外を返すようになっています。

0の場合にのみ git pushが実行できるようになります。

$ cp .git/hooks/pre-push.sample .git/hooks/pre-push
$ chmod +x .git/hooks/pre-push
[.git/hooks/pre-push]
#!/bin/sh

yarn test

ただ、この方法ではプロジェクトメンバーへの共有が難しくなります。テンプレ作って pre-push書き換えてと案内するのは面倒ですね。

husky使ったpush前テスト

huskyをinstallして package.jsonにpush前に実行したいコマンドを書くだけです。(.huskyrcを用意しても良いです)

$ yarn add --dev husky

pre-commitなどのhookも用意されています。コミット単位でテストするのは非効率なので pre-pushを使います。

package.json
{..."husky":{"hooks":{"pre-push":"yarn test"}},"devDependencies":{"husky":"^3.1.0"}}

push時に自動でテストが走り、失敗するとpushできないようになりました。

$ git push

husky > pre-push (node v10.14.1)
yarn run v1.17.3
$ yarn test
...
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
husky > pre-push hook failed (add --no-verify to bypass)

プロジェクトメンバーはパッケージインストール時にpre-pushが書き換わる

clone時のhooksは何も登録されていないsample状態ですが、

$ git clone xxxxx
$ ls .git/hooks
applypatch-msg.sample     fsmonitor-watchman.sample pre-applypatch.sample     pre-push.sample           pre-receive.sample        update.sample
commit-msg.sample         post-update.sample        pre-commit.sample         pre-rebase.sample         prepare-commit-msg.sample

パッケージインストール後に書き換えてくれます。

$ yarn
yarn install v1.21.1
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
✨  Done in 2.29s.

$ ls .git/hooks
applypatch-msg            fsmonitor-watchman.sample post-merge                post-update.sample        pre-commit                pre-push.sample           pre-receive.sample        sendemail-validate
applypatch-msg.sample     post-applypatch           post-receive              pre-applypatch            pre-commit.sample         pre-rebase                prepare-commit-msg        update
commit-msg                post-checkout             post-rewrite              pre-applypatch.sample     pre-merge-commit          pre-rebase.sample         prepare-commit-msg.sample update.sample
commit-msg.sample         post-commit               post-update               pre-auto-gc               pre-push                  pre-receive               push-to-checkout

まとめ

必ず実行するであろうパッケージインストール後に書き換えてくれるので勝手に共有できるのは良いですね。

githooks管理ツールは他にもpre-commitovercommitなどがありますが、 huskyはシンプルで良い感じです。

Spotify APIを使ってSlackから音楽を検索できるbotを作る

$
0
0

この記事はぷりぷりあぷりけーしょんず Advent Calendar 2019の23日目の記事です。

はじめに

好きなバンドとかアーティストを共有したいときってありませんか?わたしはあります。
そういったときにSlack上でサクッと検索ができたら幸せだなあと思い、アーティスト名を入力すると、そのアーティストの人気曲Top10を返すSlack botを作りました。

Infrastructure as Code の学習も兼ねて、Lambda + API Gateway はCDKで定義しています。

使用技術

Slack Outgoing Webhook
Spotify API
AWS Lambda
Amazon API Gateway
AWS CDK 1.19.0
Node.js 12.8.1

※ CDK, Node の環境構築は完了していることを前提とします。

アーキテクチャ

アーキテクチャはこのようなイメージです。

purivisual.png

ざっくり説明すると、SlackのOutgoing WebhookからAPI GatewayにPOSTし、受け取った文字列をもとにSpotify APIに検索をかけて、その結果をSlackに返すようになっています。

ディレクトリ構成

CDKでプロジェクトを作成するので、ディレクトリ構成はこのようになります。

├── bin
│   └── 〇〇-cdk.ts
├── lambda
│   ├── node_modeules
│   ├── package.json
│   ├── package-lock.json
│   └── search.js
├── lib
│   └── 〇〇-stack.ts
├── node_modeules
├── README.md
├── package-lock.json
├── package.json
└── tsconfig.json

Spotify APIを使えるようにする

Spotify APIを使用するためには、クライアントアプリケーションを作成して認証を通す必要があります。

下記URLからダッシュボードにサインアップ / ログインすることができます。
https://developer.spotify.com/dashboard/

ダッシュボードにログインができたら、CREATE AN APPをクリックしてクライアントアプリを作成します。

スクリーンショット 2019-12-21 14.41.52.png

スクリーンショット 2019-12-21 14.48.19.png

アプリが作成されたら、Client IdClient Secretをメモしておきます。

Spotify APIには3種類の認証方法がありますが、今回はクライアント側から直接APIを叩く構成ではないため、Client Credentials Flowという認証フローを採用しています。

各認証方法についての詳細はこちらをご参照ください。
https://developer.spotify.com/documentation/general/guides/authorization-guide/

CDKでAWSリソースを定義していく

AWSリソースと、Lambdaにデプロイするコードを書いていきます。

Nodeのプログラムを作る

/lambdaディレクトリを作成し、そのディレクトリでnpm initでpackage.jsonを作成したら、

npm install requestで request のパッケージをインストールしておきます。

/lambda/search.js
exports.handler=(event,context,callback)=>{constrequest=require('request');constauthOptions={url:'https://accounts.spotify.com/api/token',headers:{'Authorization':'Basic '+process.env.ACCESS_TOKEN},form:{grant_type:'client_credentials'},json:true};request.post(authOptions,function(error,response,body){if(error){console.log('POST Error: '+error.message);return;}consttoken=body.access_token;constartist=event.text.slice(5);constencoded=encodeURIComponent(artist);constoptions={url:'https://api.spotify.com/v1/search?q='+encoded+'&type=artist&market=JP&limit=1&offset=0',headers:{'Authorization':'Bearer '+token},json:true};request.get(options,function(error,response,body){letres={};if(error){console.log('GET Error: '+error.message);res.text='検索に失敗しました。ごめんなさい!';callback(null,res);}else{res.text=body.artists.items[0].external_urls.spotify;callback(null,res);}});});};

SlackからPOSTされた文字列はevent.textで取得することができます。Outgoing Webhookのトリガーとなる文字列を除くためslice(5)としています。

あとはその文字列(アーティスト名)をエンコードして、https://api.spotify.com/v1/searchのクエリパラメータに含めてリクエストをするだけです。

APIの細かい仕様はこちらをご参照ください。

https://developer.spotify.com/documentation/web-api/reference/search/search/

レスポンスが複数の場合もありえますが、今回は1件目だけをSlackに返すようにしています。

Lambdaを定義する

npm install @aws-cdk/aws-lambdaでLambdaのライブラリをインストールしたら、/lib配下のtsファイルにコードを書いていきます。

/lib/〇〇-stack.ts
importcdk=require('@aws-cdk/core');importlambda=require('@aws-cdk/aws-lambda');import{Duration}from'@aws-cdk/core';exportclassPurivisualSearchCdkStackextendscdk.Stack{constructor(scope:cdk.Construct,id:string,props?:cdk.StackProps){super(scope,id,props);// lambdaconstsearch=newlambda.Function(this,'SearchHandler',{runtime:lambda.Runtime.NODEJS_10_X,code:lambda.Code.fromAsset('lambda'),handler:'search.handler',timeout:Duration.minutes(1),environment:{"ACCESS_TOKEN":"< your access token >"}});}}

environment< your access token >には、Spotifyでアプリを作成したときに発行したClient IdClient Secretをbase64でエンコードした文字列を入れてください。

ターミナルで下記コマンド叩くとエンコードした文字列を出力できます。

echo -n < Client Id >:< Client Secret > | base64

API Gatewayを定義する

npm install @aws-cdk/aws-apigatewayでAPI Gatewayのライブラリをインストールします。

/lib/〇〇-stack.ts
importcdk=require('@aws-cdk/core');importlambda=require('@aws-cdk/aws-lambda');importapigw=require('@aws-cdk/aws-apigateway');import{Duration}from'@aws-cdk/core';exportclassPurivisualSearchCdkStackextendscdk.Stack{constructor(scope:cdk.Construct,id:string,props?:cdk.StackProps){super(scope,id,props);// lambdaconstsearch=newlambda.Function(this,'SearchHandler',{runtime:lambda.Runtime.NODEJS_10_X,code:lambda.Code.fromAsset('lambda'),handler:'search.handler',timeout:Duration.minutes(1),environment:{"ACCESS_TOKEN":"< your accsess token >"}});// api gatewayconstapi=newapigw.LambdaRestApi(this,'PurivisualSearchApi',{handler:search,proxy:false});// リソースの作成constpostResouse=api.root.addResource("serach")constresponseModel=api.addModel('ResponseModel',{contentType:'application/json',modelName:'ResponseModel',schema:{}});consttemplate:string='## convert HTML POST data or HTTP GET query string to JSON\n'+'\n'+'## get the raw post data from the AWS built-in variable and give it a nicer name\n'+'#if ($context.httpMethod == "POST")\n'+' #set($rawAPIData = $input.path(\'$\'))\n'+'#elseif ($context.httpMethod == "GET")\n'+' #set($rawAPIData = $input.params().querystring)\n'+' #set($rawAPIData = $rawAPIData.toString())\n'+' #set($rawAPIDataLength = $rawAPIData.length() - 1)\n'+' #set($rawAPIData = $rawAPIData.substring(1, $rawAPIDataLength))\n'+' #set($rawAPIData = $rawAPIData.replace(", ", "&"))\n'+'#else\n'+' #set($rawAPIData = "")\n'+'#end\n'+'\n'+'## first we get the number of "&" in the string, this tells us if there is more than one key value pair\n'+'#set($countAmpersands = $rawAPIData.length() - $rawAPIData.replace("&", "").length())\n'+'\n'+'## if there are no "&" at all then we have only one key value pair.\n'+'## we append an ampersand to the string so that we can tokenise it the same way as multiple kv pairs.\n'+'## the "empty" kv pair to the right of the ampersand will be ignored anyway.\n'+'#if ($countAmpersands == 0)\n'+' #set($rawPostData = $rawAPIData + "&")\n'+'#end\n'+'\n'+'## now we tokenise using the ampersand(s)\n'+'#set($tokenisedAmpersand = $rawAPIData.split("&"))\n'+'\n'+'## we set up a variable to hold the valid key value pairs\n'+'#set($tokenisedEquals = [])\n'+'\n'+'## now we set up a loop to find the valid key value pairs, which must contain only one "="\n'+'#foreach( $kvPair in $tokenisedAmpersand )\n'+' #set($countEquals = $kvPair.length() - $kvPair.replace("=", "").length())\n'+' #if ($countEquals == 1)\n'+'  #set($kvTokenised = $kvPair.split("="))\n'+'  #if ($kvTokenised[0].length() > 0)\n'+'   ## we found a valid key value pair. add it to the list.\n'+'   #set($devNull = $tokenisedEquals.add($kvPair))\n'+'  #end\n'+' #end\n'+'#end\n'+'\n'+'## next we set up our loop inside the output structure "{" and "}"\n'+'{\n'+'#foreach( $kvPair in $tokenisedEquals )\n'+'  ## finally we output the JSON for this pair and append a comma if this isn\'t the last pair\n'+'  #set($kvTokenised = $kvPair.split("="))\n'+' "$util.urlDecode($kvTokenised[0])" : #if($kvTokenised[1].length() > 0)"$util.urlDecode($kvTokenised[1])"#{else}""#end#if( $foreach.hasNext ),#end\n'+'#end\n'+'}'// POSTメソッドの作成postResouse.addMethod("POST",newapigw.LambdaIntegration(search,{// 統合リクエストの設定requestTemplates:{'application/x-www-form-urlencoded':template},// 統合レスポンスの設定integrationResponses:[{statusCode:'200',contentHandling:apigw.ContentHandling.CONVERT_TO_TEXT,responseTemplates:{'application/json':"$input.json('$')"}}],passthroughBehavior:apigw.PassthroughBehavior.WHEN_NO_MATCH,proxy:false}),// メソッドレスポンスの設定{methodResponses:[{statusCode:'200',responseModels:{'application/json':responseModel}}]})}}

apigw.LambdaRestApi()のhandlerに先ほど定義したLambdaを指定してあげることで、LambdaをバックエンドとしたAPIを作成することができます。

SlackからPOSTされるデータはapplication/x-www-form-urlencoded形式のため、jsonに変換しています。AWSフォーラムで紹介されているマッピングテンプレートをまるっとコピーして使用しています。
https://forums.aws.amazon.com/thread.jspa?messageID=673012&tstart=0#673012

デプロイ

これでAWSリソースとLambdaにデプロイするプログラムが完成したので、デプロイします。

cdk diff
cdk deploy

SlackのOutgoing Webhookを設定する

スクリーンショット 2019-12-21 17.29.53.png

「引き金となる言葉」には被ることがないような文言を設定しておくのが無難です。わたしは推しの名前にしました。

あとは、URLに作成したAPI Gatewayのエンドポイントを指定し、名前やアイコンなどを設定して保存すればbotの完成です。

スクリーンショット 2019-12-21 12.15.05.png

このように、「SORA」のあとにスペース+アーティスト名で、そのアーティストの人気曲Top10が表示されるようになりました!残念ながらSlack上で再生ができるは視聴版のみとなっており、全曲フル尺で再生したい場合はリンクからSpotifyを開く必要があります。

最後に

こういうbotがあれば「このアーティストオススメだから聴いてみて!」が簡単にできて楽しいかなと思って作ってみました。
あとbotのアイコンを推しにするとかなり愛着が湧きます!
Spotify APIは他にも色んなエンドポイントが用意されているので、気になる方は是非使ってみてください!


BoxにあるファイルをLINE WORKSトークルーム(チャット)で共有

$
0
0

はじめに

BoxとLINE WORKSって連携してないんですか?
ってすっごいよく聞かれるので(仕事柄?)、Boxのウェブアプリ統合という機能を使って、BoxとLINE WORKSを連携機能を開発する記事を書きます。
連携と言っても、Boxから直接LINE WORKSのトークルームへファイルを共有するだけです。

完成動作イメージ

①Boxのファイルを選んで、[・・・]-[統合]を選んで、[Share with LW](←名前は任意で変えられます)をクリック。
image.png

②LINE WORKSのトークへ送信する画面が開きます。ここでは、[本文]にBoxの共有リンクを自動で挿入しています。送る相手はその都度変わると思うので、連絡先を検索するかトークルームを検索するかして宛先を設定し、[送信]ボタンをクリック。
image.png

③すると、LINE WORKSのトークでBoxの共有リンクが送られます!
image.png

開発手順

  1. Boxでウェブアプリ統合を作る
  2. HerokuでWebサーバーをたてる
  3. Boxからの通信を受けてLINE WORKSのトークルームへ投げるプログラムを書く
  4. プログラムをHerokuへデプロイ

Boxでウェブアプリ統合を作る

Box Developer アカウント作成

  1. Box Developerサイトへアクセスし、中央の[開始する]をクリックします。 image.png
  2. サインアップします。 image.png
  3. 確認メールが送られます。 image.png
  4. メールの確認をします。 image.png
  5. Boxのログイン画面が表示されるので、ログインします。
  6. 設定が完了したら、Developerコンソールへアクセスします。

新規アプリケーションの作成

1. [アプリの新規作成]をクリックします。
image.png
2.[カスタムアプリ]を選択して、[次へ]をクリックします。
image.png
3.認証方法では、[標準OAuth2.0(ユーザー認証)]を選択して、[次へ]をクリックします。
image.png
4.アプリケーションに適当な名前をつけて[アプリの作成]をクリックします。
5.DeveloperTokenが発行されますが、ここでは使わないので、[アプリの表示]をクリックします。
image.png

ウェブアプリ統合の作成

1.作成したアプリケーションを開きます。
2.左のメニュー[統合]をクリックし、[新しいWebアプリ統合を作成]をクリックします。
必要な情報を入力していきます。
詳細な情報はBoxのドキュメントを参照してください。

  • アプリ情報
    image.png
    image.png

  • コールバック設定
    [クライアントコールバックのURL]は後に作成するHerokuのURLを入れるため、ここではダミーでhttps://localhostなどとダミーの値を入れておきます。
    image.png

  • コールバックパラメータ
    ここで設定した値は、Boxから自分のWebアプリケーションへ統合メニューをクリックすると送られます。
    image.png

  • 認証
    image.png

  • 統合ステータス
    テストが完了して公開するまでは、開発者だけに統合メニューが表示されるよう「開発」を選んでおきましょう。
    image.png

全て設定が終わったら右上の[変更を保存]をクリックし保存します。

HerokuでWebサーバーをたてる

Heroku環境構築

Herokuは日本語の資料がありましたので、こちらのサイトを参考にして環境構築をしてください。

https://github.com/herokaijp/devcenter/wiki/quickstart

この時点では、「ステップ 3: ログイン」まで実施すれば大丈夫です。

Herokuアプリケーション作成

1.HerokuCLIを使ってHerokuにログインします。

$ heroku login
Enter your Heroku credentials.
Email: <your email address>
Password (typing will be hidden): 
Logged in as <your email address>

2.Herokuでアプリケーションを作成します。
アプリケーション名は、Herokuの中で一意である必要があります。

$ heroku apps:create <アプリケーション名>
Creating ⬢ <アプリケーション名>... done
https://<アプリケーション名>.herokuapp.com/ | https://git.heroku.com/<アプリケーション名>.git 

3.Gitリポジトリを初期化します。

$ git init

4.リモートリポジトリとしてHerokuを追加します。

$ git remote add heroku https://git.heroku.com/<アプリケーション名>.git

5.Git管理対象外ファイル(.gitignore)を作成します。

.DS_Store
.gitignore
npm-debug.log
node_modules

6.Herokuがプログラム起動する際に実行するファイル(Procfile)を作成します。

web: node index.js

Herokuアプリケーションへ環境変数をセット

Boxの認証情報をHeroku環境変数としてセットしておきます。
セットする資格情報は下記の通りです。
Boxアプリの「クライアントID」「クライアント秘密コード」はBoxDeveloperコンソールの作成したアプリの[構成]-[OAuth 2.0資格情報]欄にあります。

KEY補足
BOX_CLIENT_IDBoxアプリのクライアントID
BOX_CLIENT_SECRETBoxアプリのクライアント秘密コード

Herokuへ環境変数を登録するコマンドは下記です。

$ heroku config:set <KEY>="<VALUE>"

Boxからの通信を受けてLINE WORKSのトークルームへ投げるプログラムを書く

次は、index.jsを新規作成して、コードを書き、デプロイします。

コーディング

BoxのNode.js SDKを利用します。

https://github.com/box/box-node-sdk/

以下はサンプルプログラムです。
(あくまで動作を確認するためのサンプルなので、業務で使う場合はセキュリティ等の検討を十分に行ってください。
実稼働環境におけるベスト・プラクティス: セキュリティー)

index.js
//Import Modulesvarexpress=require('express');varserver=express();varexpressSession=require('express-session');varBOX_CLIENT_ID=process.env.BOX_CLIENT_ID;varBOX_CLIENT_SECRET=process.env.BOX_CLIENT_SECRET;varPromise=require('bluebird');varBoxSDK=require('box-node-sdk');varbodyParser=require('body-parser');varbasicAuth=require('basic-auth-connect');varhelmet=require('helmet');varrequest=require('request');// Setup Restify Serverserver.use(bodyParser.urlencoded({extended:true}));server.use(expressSession({secret:'keyboard cat',resave:true,saveUninitialized:false}));server.use(helmet());server.listen(process.env.port||process.env.PORT||3978,function(){console.log('%s listening to %s',server.name,server.url);});server.get('/api/box/sharewithlw',function(req,res){try{if(!req.query.auth_code){returnres.status(404).json({error:{message:'Error!'}});}varauthCode=req.query.auth_code;if(!req.query.file_id){returnres.status(404).json({error:{message:'Error!'}});}varfileId=req.query.file_id;}catch(err){returnres.status(400).json({error:{message:err.message}});}varsdk=newBoxSDK({clientID:BOX_CLIENT_ID,clientSecret:BOX_CLIENT_SECRET});vartokenInfo={};varpromise=newPromise(function(resolve,reject){sdk.getTokensAuthorizationCodeGrant(authCode,null,function(err,res){if(err){reject(err);}tokenInfo=res;resolve();});});promise.then(function(){varclient=sdk.getBasicClient(tokenInfo.accessToken);// 共有リンク作成client.files.update(fileId,{shared_link:client.accessLevels.DEFAULT}).then(function(file){varsharedLinkURL=file.shared_link.url;//LINE WORKSのトークルームへ共有する用のURLを作成varlwurl="https://talk.worksmobile.com/sendMessage?initContent="+encodeURIComponent("boxの共有リンクを送ります。\n")+"&initLink="+encodeURIComponent(JSON.stringify({"url":sharedLinkURL,"text":sharedLinkURL}))+"&serviceId=works#!/";res.redirect(301,lwurl);});}).catch(function(err){console.error(err);});});

プログラムをHerokuへデプロイ

Herokuへデプロイします。

$ git add . && git commit -m "<your message>" && git push heroku master

ログを確認する場合は、下記コマンドです。

$ heroku logs --tail

Boxへ[クライアントコールバックのURL]をセット

  1. BoxのDeveloperコンソールを開き、作成したアプリを開きます。
  2. [統合]メニューの[コールバック設定]の[クライアントコールバックのURL]に、HerokuのアプリケーションURL+エンドポイント(https://<アプリケーション名>.herokuapp.com/api/box/sharewithlw)を入力し、[変更を保存]します。 image.png

テスト

Box上でLINE WORKSへ共有したいファイルを選んで、[・・・]→[統合]→[Share with LW]をクリックしてみてください!
完成動作イメージと同様の動作をしたら成功です!

LINE WORKS + Box連携

今回は、BoxのファイルをLINE WORKSのトークルームを選んで共有するというメニューを開発してみました。
このメニューがない場合どうやって共有するかというと、
Boxの共有ボタンをクリックして、共有リンクを発行して、共有リンクをコピーして、LINE WORKSのトークルームを開いて、共有リンクを貼り付けて、送信!と、めんどくさいですよね・・・
これらの操作をBoxのWebアプリ統合メニューを使うと、自動化できるので、興味ある方は是非チャレンジしてみてください。

思いもよらないものをnpm publishしてしまった話(前任者の顔写真など)

$
0
0

スクリーンショット 2019-12-23 0.06.32.png
俺「すっげぇいい名前のライブラリ思いついた!!」

俺「npm あるかな?あるかな?」

(カタカタ)

俺「なかった!よっしゃ一番乗りや!!!今すぐ作らないと!!!」

俺「npm init enter enter enter enter npm publishうおおおおおおおいっけぇぇぇ!!!!!!!」

(カタカタカタカタカタカタカタカタッターン)

俺「ミ゜ッ!」

ーーー

おはようございます。本番環境でやらかしちゃった人 Advent Calendar 2019の 23 日目を担当する、@sandessOjisanです。この記事では 思いもよらないものを npm publish したお話を紹介します。

僕は あるとき 色々な会社の仕事を請け負っていた時期があり、そのときの無邪気なnpm publishによって、良くないことをしてしまったというお話しです。このコマンドを叩くといきなり本番環境に上がるので、1ミスが命取りです。僕が何をしでかしたか、ここでだいたい想像ついた方もいらっしゃると思いますが、その方はニヤニヤしながらお楽しみください。あと、ところどころ誤魔化しているのは意図的であり、また現在の所属でのやらかしではありません。

では、やらかしを 2 つと、その対策について紹介します。

ツールを publish したら、Collaborators に前任者の顔写真が上がった話

npm のざっとした説明

普段 JS を書かない方も読まれていると思いますので、僕がやらかした npm publishが何かについて説明します。npmは Node Package Manager の略で、主に JS プロジェクト のための、パッケージ・ライブラリ管理システムです。 ライブラリは npm registry に登録することで、配布や共有が可能になります。そして、npm publishというコマンドが npm registry に登録するコマンドです。

npm に登録すると、このようなページが作られます。(react の npm ページ)

react.png

このとき、開発者は collaborators として登録されます。

collaborators.png

普通、ここには自分で作った ライブラリ であれば自分のアイコンが表示されるはずです。しかし僕がライブラリを作った時、ここに僕のアイコンはありませんでした。

自分が作ったライブラリの collaborators 欄に前任者の顔写真が載った話

あるとき、某社の某チームにて、ちょっとしたライブラリを作っていました。それはその会社のいろんなプロジェクトから呼び出されるものであり、ライブラリ(node_module)として開発されていました。そして、どうしてか npm 経由で配布していました。1

僕はそのツールをメンテナンスする仕事をしていたのですが、どうしても必要なモジュールがあったので、それを別ライブラリとして開発、publish しようとしました2。このときの publish の方法は、

  1. 責任者から npm publish用の共用アカウントの id/password を教えてもらい
  2. それをターミナルに打ち込んで手元から publish する

というものでした。(※そもそもその運用がヤバいだろというツッコミはお控えください。あとこの運用しているところ何個か知っているので、別に珍しい話でもないです。ただ、良くない運用です。)

早速 publish しようと id/password を打ち込んでログインし、npm publishを行いました。どんな風にページができるかたまたま気になったので npm ページで成果物を確認しに行きました。なんとページを開くとそこには前任者の顔写真がありました。

pose_dance_ukareru_man.png

このときプロジェクトの成果物を前任者の顔写真に紐づけてしまったので、

  • 会社の看板を背負って、人の顔写真をあげてしまった
  • 前任者の顔写真をその名の通りグローバルなインターネットに公開してしまった

というやらかしをしたことに気づき、すごい冷や汗が出てきました。が、責任者や前任者にそのことを報告するとゲラゲラ笑われたので結果的には大丈夫でした。そこで余裕をもって原因を解明してみました。

何がおきたのか

最初は package.json を疑いました。何かをコピペしてきて、author のところも変にコピペしたかなといった風に思って package.json を確認しましたが何も問題はありませんでした。そもそもこの author と collaborars は関係なさそうです。なので、authorである自分の写真が表示されないことはそもそも正常な挙動でした。

さらに調査するために collaborators のところからその前任者のページに飛ぶと、そこには共有アカウントの名前が表示されていました。なんと、そもそもプロジェクトの共有アカウントそのものに前任者の顔写真が紐づいていました。つまり、僕がやらかす前にすでに前任者自らが、共用アカウントに自分の顔写真を紐づけるというやらかしをしていました3。では、どうして前任者は共用アカウントに自分の顔写真を紐づけてしまったのでしょうか。

Gravatar

npm はGravatarというアバター作成サービスをアイコンとして使っています。そして、npm のアイコンは Gravatar からしか登録できません(Issue#12)。つまりアカウント作成時や編集時になんらかのやらかしがあったようです。

gravatar_small-262x135.jpg

共用アカウントを作ったのは前任者です。そしてそのときに 自身の Gravatar と npm のアイコンを紐けてしまったようです。前任者になんで紐づけたかを確認したところ、それは分からないとのことでした。

推測でしかないですが、Gravatar の操作は難しく、前任者があやまった設定をしただけな気がしています。設定は このような手順でやっていくのですが、レーティングの設定や、何を持って別サービスと Gravatar のアイコンを紐づけているのかが不明瞭で、意外と苦戦します。npm は、一般的なサービスと違って、自分で写真をアップしてそれを選択してアイコンとして設定するといったことができません。そのため、なんらかの操作ミスによってこういう事故はおきてしまったのだと思います。

これはまだ笑い話ですが、次は本当のやらかし話です。

個人開発した OSS を A 社のアカウントに紐づけて publish した話

僕はジョークライブラリを npm に publish することが趣味で、日頃から publish しています。先日も ビルド時に俳句を読めるプラグインを publish したり、その前にはgeo cities コンポーネントなどを publish しました。よく友人からは「電子ゴミ職人」 と呼ばれています。

僕は、このようなジョークライブラリを、とある会社のアカウントで publish してしまいました。

なぜ自分以外のアカウントで publish したか

それは様々な会社で npm publishしている関係で、色々な会社のアカウントに npm loginしているからです。そしてそれらの publish をプライベート端末で行っているので、会社のアカウントに login したまま自分の OSS を publish し、自分じゃないアカウントに紐づけてしまったからです。

npm は publish 前に 「このアカウントで publish しますよ y/N」といった確認もないので、いきなり本番環境に publish できてしまいます。そのため、いまどのアカウントでログインしているかを確認しないと、全然関係ない組織に紐づけて publish してしまうということが起き得ます。そして僕は見事にやらかしました。真面目な事業をしている会社のアカウントで、ジョークライブラリ以外の説明ができない名前のライブラリを publish してしまいました。

chikyu_inseki_syoutotsu.png

たまたま、すぐに気づいたので npm unpublishして、責任者に報告と謝罪をしました。その後は、すぐさま再発防止策とその運用フローを用意しました。その再発防止策は記事の終盤で紹介します。

ぶっちゃけ間違って publish しても消せばセーフなのでは?

アウトです。確かにやらかしても npm unpublishすれば消せるので、このケースもやらかしではないかもしれません。ただ、この npm unpublishは時間制限があるので注意が必要です。なんと、公開から72 時間後は消せなくなります。(npm-unpublish)。72 時間たつと、サポートにメールで問い合わせしないといけなく、簡単な道のりではなさそうです4。また、そもそも消しても、消すまでに多少のラグはあり、その間に第三者に見られたり install されると、配信元の信用低下に繋がり兼ねません。

なぜ npm から消せないのか

どうしてこのような厳しいルールになったのか直接的な理由は知りません。ただ、よく言われているのは、left-pad 騒動です。これは、left-padというライブラリが作者の意向で非公開となったとき、それに依存していた他のライブラリのビルドも通らなくなり、その影響が波及し利用者の多い有名サービスのビルドですらできなくなったというものです。また、有名ライブラリが消えるとその名前を横取りしてユーザーに悪意のあるコードを DL させるということもできます。5そういった事故が起きないように、npm 社は package の削除に対して慎重な姿勢になっています。

そういった運営がされる以上、もしあなたが何か npm registry 上で情報漏洩して 72 時間経つと、半永久的にあなたが情報漏洩した事実が記録されます。注意しましょう。

left-pad 騒動について

left-pad の一連の騒動については以下の記事などで解説されています。興味のある方はご覧ください。

[作り話] もし仮に A 社アカウント login 中に B 社 prj のディレクトリで publish したら何が起きるか (情報漏洩者として名が一生涯 npm に刻まれる話)

先ほどの話は、自分で作った OSS を npm で公開した話です。もし仮に、2 社かけもちで仕事をしていて、A 社の成果物を B 社に紐づけて publish したらどうなっていたでしょうか。情報漏洩です。

npm は、その気になれば、

  1. A 社アカウントでログイン
  2. B 社プロジェクトの dir 階層に移動
  3. package.json の名前を適当に書き換える
  4. npm publish
  5. A 社の名前で B 社のソースコードを全世界に公開する

ができてしまいます。

これは完全に悪意を持った行為ですが、先ほどの僕のように無邪気なnpm publishで同じようなことが起きることも十分に考えられます6npm publishは慎重に使うべきコマンドです。

惨劇はなぜおこってしまったのか

npm publish が悪い?

内心では、「何も確認もなく npm publish できてしまうの、ちょっとカジュアルすぎるのでは!?!?」と思っています。とはいえ、npm に限らずこの手のツールはこの手のやらかしをできてしまうので、一般的なプロセスとも言えます。結局は使う側が注意しなければいけません。7

軽い気持ちで publish する自分が悪い

では、なぜ僕はすぐにちゃんとした確認もせずに pubilsh してしまったのでしょうか。それはすぐデプロイしたくなる力が働いたからです。

とりあえず deploy したくなる

  • パッケージ名 は早い者勝ちなので、とりあえず v0.0.1 ですぐ publish したい
  • 本番へのデプロイが安定するまでの修正は、本番に対するトライアンドエラーになる

1 番目はともかく(マナー的にもよくない気がするし、可能ならnamespace使えばいい8)、2 番目はどうしても起きる問題なため、カジュアルな deploy はできた方が良さそうです。その上でどう対策していけば良さそうでしょうか。

二度と惨劇を起こさないためにどうしたのか

結局のところ、個人が自分の裁量だけで publish できるのがよくないのだと思います。

publish 権限を個人に割り当てるのをやめよう

まずは個人端末からの publish を制限すべきだと思います。

そして、そもそも OSS にするつもりがないものを npm で管理するのはやめましょう。Github をレジストリ代わりに使うこともできます。(npm install で github のリポジトリからインストールする)

対策に向けて ~個人開発でも CI/CD サーバーを用意しよう~

結論から言うと CI/CD から publish しようということです。そもそもの話なのですが、ローカルからレジストリに push ってやらない行為だと思っています。Node.js に限らずレジストリにライブラリを登録していくとき、普通は CI/CD サーバー からやっているはずです。9

ところが個人レベルでは CI/CD を使って module を publish している人は意外と少ないのではないでしょうか。そこで、個人レベルでも CI/CD サーバーを導入する対策方法について紹介します。ズバリ、Github Actions を使いましょう。

npm publish の仕組み

npm publish のためには、まず npm に登録して、npm login する必要があります。これまではこの無邪気なログインでやらかしをしていました。

認証と token

npm loginすると、 ~/.npmrcというファイルに token が保存されます。npm publish時はこの token を利用して認証をクリアしています。そのため試しにこのファイルを適当な文字で書き換えてみると publish ができなくなります。

この token はコピーしちゃえば持ち出せるので、これを CI/CD サーバーに配置すれば npm publishを行うことが可能となります。

module と package.json

ところで npm publish をしたら何が公開されるのでしょうか。基本的には package.json に書かれている内容が公開されます。特に name と version が重要です。この name はそのまま publish 時の名前になり、version は name ごとに同一なものが存在できません。そのため npm publishするときは適切な名前をつけて、publish ごとにバージョンを変える必要があります。またここで登場する author は collaborators には反映されません。collaborators は npm loginのユーザーに紐づくことに注意しましょう。

Github Actions で対策してみる

原理的には認証トークンの配置と、package.json の記述をすれば CI/CD サーバーからでも npm publishできます。ここでは Github Actions での例を示しますが、Circle CI でも drone といった他の CI/CD ツールでも可能です。

Github Actions はその名の通り、Github が提供している デプロイワークフロー自動化サービスです。Github と連携しているため、 .github/workflow/ 配下に workflow ファイルを設置するだけで CI/CD パイプラインを構築できます。

これは個人レベルでも CI/CD パイプラインを簡単に導入できる素晴らしいものです。これがあれば個人開発中に副業先のソースコードをインターネットにぶちまける危険性を下げることができます。10

workflow で publish を定義

CI/CD パイプラインにおける各種タスクはワークフローファイルという yaml に記述して定義します。
prd のデプロイで publish すればよさそうです。とりあえず、tag が打たれたら、それに該当するコードがビルドされてデプロイされるように設定します。

name:prdon:push:tags:-v*jobs:build:runs-on:ubuntu-lateststrategy:matrix:node-version:[12.x]steps:-uses:actions/checkout@v1-name:Use Node.js ${{ matrix.node-version }}uses:actions/setup-node@v1with:node-version:${{ matrix.node-version }}-name:installrun:|yarn install-name:buildrun:yarn build:prd-name:publishrun:|npm config set //registry.npmjs.org/:_authToken=$NPM_TOKENnpm publishenv:CI:trueNPM_TOKEN:${{ secrets.NPM_TOKEN }}

ここでは環境変数として NPM_TOKEN をあらかじめ Github の設定画面で設定しておき、それを config として読み込むようにしています。これは実質的にはログインに相当します。
そして、npm publishを実行しています。

むやみな publish を防ぐワークフロー定義

これでタグ打ちから publish できるようになりましたが、タグ打ちのための手間はかかるので、publish は失敗したくないです。そのためなるべく確実に publish できるように諸々のチェックを master ブランチマージ前に行うようにしましょう。そのワークフローを開発用 workflow として設定を書きます。

name:devon:push:branches:-feature/**-fix/**jobs:build:runs-on:ubuntu-lateststrategy:matrix:node-version:[12.x]steps:-uses:actions/checkout@v1-name:Use Node.js ${{ matrix.node-version }}uses:actions/setup-node@v1with:node-version:${{ matrix.node-version }}-name:installrun:|yarn install-name:licence checkrun:|summary=`yarn license-checker --production --summary`echo $summaryret=`echo $summary | grep -v LGPL | grep -e GPL | wc -l`exit $ret-name:buildrun:|yarn build:devenv:CI:true-name:can i publishrun:yarn run can-npm-publish

ここでは feature/** や fix/** ブランチへの push があると、publish できるかチェックしています。そして Github 側でこのワークフローが全部通らないと merge できないようにすれば、master ブランチのソースコードは publish の成功確率が上がります。11

ここでは、ライセンスの確認と、バージョン上げ忘れの確認をしています。特にこの can-npm-publishは諸々の確認をしてくれるので便利です。

またやらかした

こちらがやらかなさないプロジェクトとして作っていたものですが、やらかしてます。コミットログを見ればわかると思いますが、やらかしまくってます。例えば、ライセンス違反をして publish をしたり、package 名が規則にあっておらず空振りしたり、namespace で区切ったが access 設定してなくて publish に失敗したり、エントリポイントの path が間違っていて何も import できないなどやらかしています。npm publishが成功したかはどうしても本番でしか確認できないので、stable なデプロイができるようになるまでは本番でやらかす傾向になります。stable にする前はマイナーバージョンなので良い気もしてますが、もしこのタイミングで npm install されると、ユーザー側で不具合がおきる可能性があります。そのため CI/CD サービスを使ったデプロイワークフロー整備も大切ですが、本番だけでこけることも想定した対策も必要です。12


  1. そもそも社内ツールを npm に公開しちゃダメだと思うんですよね・・・ 

  2. Node.js には小さなモジュールという教義があり、分けれるものは分けた方がよいという主張があります。 

  3. そのため俺はやらかしていないのかもしれない 

  4. サポートに連絡したことないので本当は簡単な道のりなのかもしれないです。詳しい人いたら教えてください。 

  5. たまに有名ライブラリに見せかけた名前で、外部と通信しているモジュールとかも公開されていたりもするので、注意しましょう。 

  6. A, B 社双方ですでに一度でも publish していれば、名前が登録されるのでそのような事故はおきないですし、事故で公開されてもすでにインターネットで公開されている情報なので情報漏洩かどうかの観点では大丈夫です(なりすまし行為になるので全然大丈夫ではないのですが)。ただ B 社側がまだ publish していなかったり、そもそも B 社のコードがライブラリではなくアプリケーションだった場合は publish されていないものなので、情報漏洩に繋がります。 

  7. おそらくクラウドへのデプロイもこの手の事故を起こせます。(サービスの名前は伏せます。) 

  8. namespaceを区切るとpublishに失敗する可能性も上がります。(詳しくは 対策の package.jsonのpublishConfigを参照) 

  9. 僕がやらかしたところは、DevOps 的な取り組みは後回しにしていたという背景があります。そのようなチームでも記事内で説明した Github Actions を使うソリューションは効果的だと思います。 

  10. もちろん、Github Actions でなくても Github 連携できる CircleCI などを使えば同様の問題は解決できます。ただ Github Actions が今一番敷居が低いと思っているので、これを題材に選びます。 

  11. この設定は Github の setting から行える。 

  12. 具体的には、npm link や tar を組み合わせた擬似 dry run をすると良いでしょう。また stable でないことが明らかであるなら、stable でないことを README かどこかに明記しておきましょう。 

備忘録 Node.jsでexportsを使ったモジュール化の作り方

$
0
0

Node.jsには、モジュール化といってさまざまな機能を持ったプログラムを個別のファイルに分割することができる。

効率よくコードを管理できるだけでなく、さまざまなユーザーが作成したプログラムを再利用しやすくできる。

Node.jsにはこのようなモジュールを管理できるツール「npm」や「yarn」等がある。

「exports」

基本的なモジュール化の方法として「exports」がある。

exportsを使うと、指定した値を外部のNode.jsファイルから読み込んで再利用することができるようになる。

exports.プロパティ名 = 値

この値は文字列・数値・配列・オブジェクト・関数など、さまざまなものを設定できる。

exports.str = 'こんにちは';

exports.obj = {
name: 'メロン',
price: 500,
store: '東京店舗'
};

exports.func = function() {
console.log('exportsの関数');
};

「module.exports」

module.exportsを利用するのが一般的。
両者の大きな違いは、プロパティ名付与せずに値を直接格納できるか。
module.exportsの場合、下記の様にプロパティ名をつけずに値を設定できる。

module.exports = {
name: 'メロン',
price: 500,
store: '東京店舗'
};

「module.exports」の場合はmoduleオブジェクトの中にあるexportsプロパティを参照している。

exportsも同じようにデフォルト状態ではmoduleオブジェクトの中にあるexportsプロパティを参照しているが、値を直接格納してしまうとそれはただの新しいオブジェクトに変わってしまうからモジュール化にはならない。

まとめ

・exportsは任意のプロパティ名を付与することでさまざまな値をモジュール化できる

・module.exportsも同じことが実現できてプロパティ名無しでもモジュール化できる

・exportsは必ずプロパティ名を付与しないと正しく機能しない

MacにNode.jsをインストール(anyenv + nodenv編)

$
0
0

プロジェクトごとにNode.jsのバージョンを管理できる!?

っていう話を聞いたのでnodenvをインストールすることにしました。
nodebrewだとnodebrew use [バージョン番号]みたいに切り替えるのがめんどくさくて大変で。
nodenvだと簡単にできるっぽいのでこっちを使ってみます。
いろいろなプロジェクトに関わってくるとこういうのがすごくありがたい。
開発者の方に感謝です。

インストールの流れ

  1. Homebrewのインストール
  2. anyenvのインストール
  3. nodenvのインストール
  4. Node.jsのインストール

・Homebrew

HomebrewはMac用のパッケージマネージャ。
ツールのインストールとか諸々を管理してくれます。
http://brew.sh/index_ja.html
nodebrewをインストールするためにHomebrewを使います。

・nodenv

順番前後しますが、これがメイン。Node.jsのバージョン管理ツール。
https://github.com/nodenv/nodenv

・anyenv

様々なenv系ツールをまとめてくれるらしい。
https://github.com/anyenv/anyenv
nodenvだけでいいんじゃないのって思ってましたが、nodenv単体でインストールするときには環境変数など色々操作するみたいですが(そうでもない?)、anyenvを使えばあらあら簡単にインストールできるみたいです。
あと他のenv系ツール使ってたら色々把握するのに面倒だけど、anyenv使ってれば簡単に把握できるみたい。
(間違ってたらご指摘ください・・・)
ということでanyenvを使ってみます。

Homebrewインストール

まずはHomebrewから。
Mac使ってるとこれはまあ外せない。
もうインストール済みって人はスキップで。

http://brew.sh/index_ja.htmlにあるスクリプトを実行する。

/usr/bin/ruby -e"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

※ 2019/12/23現在は上記

インストール後は以下コマンドでHomebrewのバージョンが確認できます。

% brew -v

私の場合は以下でした。

Homebrew 2.1.11
Homebrew/homebrew-core (git revision be0385; last commit 2019-09-12)
Homebrew/homebrew-cask (git revision d34609; last commit 2019-09-12)

anyenvインストール

とその前にnodebrew用に設定していた環境変数をコメントアウトします。

nodebrewを使っていた人はこちらをやっておいたほうがベターかと。
使ったことないって人はスキップでOK。
僕の場合、nodebrewが優先になるように記述していたのでコメントアウトしました。

% vi .zprofile

# export PATH=$HOME/.nodebrew/current/bin:$PATH# ↑この部分をコメントアウト

viを使ってコメントアウトしました。
まあ、テキストエディタならなんでも良いと思うので適宜使いやすいやつを使ってください。

Homebrewを使ってanyenvインストール

さっそくインストールしていきましょう。

% brew install anyenv

そして初期化

% anyenv init
# Load anyenv automatically by adding# the following to ~/.zshrc:eval"$(anyenv init -)"

うーん?つまりこの一文を ~/.zshrcに記述しろっちゅうこうとなんやな!
(たぶんbashとか使っている人は~/.bashrcとかになるのかな)

ということで~/.zshrcに追記

% echo'eval "$(anyenv init -)"'>> ~/.zshrc

ターミナルを再起動してみます。
すると。。。

ANYENV_DEFINITION_ROOT(/Users/whoami/.config/anyenv/anyenv-install) doesn't exist. You can initialize it by:
> anyenv install --init

はい、しょっぱなエラーで焦ります。

You'll see a warning if you don't have manifest directory.

とマニュアルにあるのでマニフェストディレクトリ作っちゃいましょう。

マニュフェストディレクトリを作る

指示に従ってanyenv install --initと打ってみます。

% anyenv install--init
Manifest directory doesn't exist: /Users/whoami/.config/anyenv/anyenv-install
Do you want to checkout ? [y/N]: y
Cloning https://github.com/anyenv/anyenv-install.git master to /Users/whoami/.config/anyenv/anyenv-install...
Cloning into '/Users/whoami/.config/anyenv/anyenv-install'...
remote: Enumerating objects: 48, done.
remote: Total 48 (delta 0), reused 0 (delta 0), pack-reused 48
Unpacking objects: 100% (48/48), done.

途中check outするかってきたので「y(もろちん ちん!)」で承諾してます。
これでOKかな?
あってるかどうか念のためターミナルを再起動してみたら今度はエラーが出ない!!(やったー!)

nodenvをインストール

さっそくanyenvでやってみましょうぞ!
anyenv install nodenvを実行すればnodenvがインストールできます。

% anyenv install nodenv
/var/folders/sn/v1hx7kls3t3f2d9s2r40krns6qc63m/T/nodenv.20191223143232.16265 ~
Cloning https://github.com/nodenv/nodenv.git master to nodenv...
Cloning into 'nodenv'...
remote: Enumerating objects: 18, done.
remote: Counting objects: 100% (18/18), done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 3915 (delta 4), reused 13 (delta 2), pack-reused 3897
Receiving objects: 100% (3915/3915), 696.59 KiB | 1.04 MiB/s, done.
Resolving deltas: 100% (2579/2579), done.
~
~/.anyenv/envs/nodenv/plugins ~
Cloning https://github.com/nodenv/node-build.git master to node-build...
Cloning into 'node-build'...
remote: Enumerating objects: 77, done.
remote: Counting objects: 100% (77/77), done.
remote: Compressing objects: 100% (52/52), done.
remote: Total 18197 (delta 33), reused 42 (delta 11), pack-reused 18120
Receiving objects: 100% (18197/18197), 3.19 MiB | 2.46 MiB/s, done.
Resolving deltas: 100% (11638/11638), done.
~
~/.anyenv/envs/nodenv/plugins ~
Cloning https://github.com/nodenv/nodenv-default-packages.git master to nodenv-default-packages...
Cloning into 'nodenv-default-packages'...
remote: Enumerating objects: 267, done.
remote: Counting objects: 100% (267/267), done.
remote: Compressing objects: 100% (151/151), done.
remote: Total 531 (delta 151), reused 202 (delta 105), pack-reused 264
Receiving objects: 100% (531/531), 113.88 KiB | 291.00 KiB/s, done.
Resolving deltas: 100% (272/272), done.
~
~/.anyenv/envs/nodenv/plugins ~
Cloning https://github.com/nodenv/nodenv-vars.git master to nodenv-vars...
Cloning into 'nodenv-vars'...
remote: Enumerating objects: 211, done.
remote: Total 211 (delta 0), reused 0 (delta 0), pack-reused 211
Receiving objects: 100% (211/211), 31.82 KiB | 264.00 KiB/s, done.
Resolving deltas: 100% (76/76), done.
~

Install nodenv succeeded!
Please reload your profile (exec$SHELL-l) or open a new session.

インストール成功です!
最後にプロファイルのリロードのため(exec $SHELL -l)するか再起動しろと書いてるので、おしゃれにコマンド打ってみます。

% exec$SHELL-l

うひょ。envで環境変数がどうなったかみてました。

% env   
PATH=/Users/whoami/.anyenv/envs/nodenv/shims:/Users/whoami/.anyenv/envs/nodenv/bin:/usr/local/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin:/usr/local/sbin:/Users/k
akai/.nodebrew/current/bin
NODENV_ROOT=/Users/whoami/.anyenv/envs/nodenv
NODENV_SHELL=zsh

(他省略)

なんかnodenvやらanyenvのパスがPATHに追加されてますね。
NODENV_ROOTとかNODENV_SHELLみたいなのも追加されてます。
これで下準備は整いました。たぶん。

Node.jsのインストール

インストール可能なバージョンを見る

% nodenv install-l

こうすれば一覧でてきました。
すごく多いです。。。

Node.jsをインストール

以下を実行すれば指定したバージョンをインストールできます。

% nodenv install{バージョン番号}

ちなみにアンインストールは~/.nodenv/versionsあるディレクトリを削除すれば良いようです。
以下のコマンドでインストール済みのバージョンを確認できます。
shell
% nodenv whence npm

nodenv globalでコンピュータのデフォルトに指定

nodenv globalでコンピュータのデフォルトを指定してみます。
指定できるのはインストールしたバージョンです。

nodenv global {バージョン番号}

僕は10.0.0にしてみました。
確認してみましょう。

% node -v
v10.0.0

10.0.0になってるうう!

nodenv localでプロジェクト指定

nodenv localを使えばプロジェクト指定できるみたいです。
早速使ってみます。

# プロジェクトのパスに移動し
% cd{プロジェクトのパス}# 配下で実行
% nodenv local{バージョン番号}

実行するとプロジェクト配下に.node-versionっていうファイルができました。
中をみるとバージョン番号が記述されてます。

.node-version
{バージョン番号}

8.8.0を指定してみたので確認してみましょう。

% node -v
v8.8.0

しゅ、しゅごい!
すごい便利です。
これでNodeの切り替えが楽になりました。

ちなみにnodenv shellコマンドだとシェルごとに変えられるっぽいです。
便利すぎる。。。
是非みなさんも使ってみてください。

参考

nodenv
https://github.com/nodenv/nodenv

anyenv
https://github.com/anyenv/anyenv

ラズパイ4とGStreamerでストリーミングサーバーを作ろう

$
0
0

やること

ラズパイ4にカメラをつけてストリーミング配信の環境を作ります。 ラズパイに付けられるマイクがなかったので、音声なし映像のみです。

参考

構成図

カメラを付けたラズパイを、GStreamerとNodejsでストリーミングサーバにします。

更に、CORSを設定し別のWEBサーバでもストリーミングデータが利用できるようにします。

環境

  • ラズパイ
    • Raspberry Pi 4 Model B 4GB
$ lsb_release -a
No LSB modules are available.
Distributor ID: Raspbian
Description:    Raspbian GNU/Linux 10 (buster)
Release:        10
Codename:       buster
  • カメラ

参照元

カメラがサポートするフォーマット

$ v4l2-ctl --list-formats-ext
ioctl: VIDIOC_ENUM_FMT
        Type: Video Capture

        [0]: 'YU12' (Planar YUV 4:2:0)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2
        [1]: 'YUYV' (YUYV 4:2:2)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2
        [2]: 'RGB3' (24-bit RGB 8-8-8)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2
        [3]: 'JPEG' (JFIF JPEG, compressed)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2
        [4]: 'H264' (H.264, compressed)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2
        [5]: 'MJPG' (Motion-JPEG, compressed)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2
        [6]: 'YVYU' (YVYU 4:2:2)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2
        [7]: 'VYUY' (VYUY 4:2:2)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2
        [8]: 'UYVY' (UYVY 4:2:2)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2
        [9]: 'NV12' (Y/CbCr 4:2:0)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2
        [10]: 'BGR3' (24-bit BGR 8-8-8)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2
        [11]: 'YV12' (Planar YVU 4:2:0)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2
        [12]: 'NV21' (Y/CrCb 4:2:0)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2
        [13]: 'BGR4' (32-bit BGRA/X 8-8-8-8)
                Size: Stepwise 32x32 - 2592x1944 with step 2/2

このようにラズパイに接続します。(写真はイメージです。基板はラズパイ4ではありません)

GStreamerをインストールしよう

ストリーミング配信に必要なアプリGStreamerをインストールします。以下2つのコマンドを実行すればインストール完了です。(ということがわかるまで苦労しました)

$ sudo apt install autoconf automake libtool
$ sudo apt install gstreamer1.0-tools gstreamer1.0-plugins-good gstreamer1.0-plugins-ugly libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev

GStreamerの動作確認しよう

早速ストリーミングデータを作成してみます。

$ mkdir test
$ cd test
$ sudo gst-launch-1.0 -v -e v4l2src device=/dev/video0 ! video/x-h264,width=640,height=480,framerate=15/1 ! h264parse ! mpegtsmux ! hlssink max-files=8 target-duration=5 location=./segment%05d.ts playlist-location=./output.m3u8 playlist-root=/

実行時はこんな感じです。

実行すると、このように.m3u8,.tsファイルが作成されます。

$ ls
output.m3u8  segment00000.ts  segment00001.ts  segment00002.ts

GStreamerの出力ファイルを確認しよう

.m3u8の中は、.tsファイルとの相対パスが書かれてました。実際の相対パスとずれていないか確認します。(これに気が付かず苦労しました)

$ cat output.m3u8
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-ALLOW-CACHE:NO
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:9

#EXTINF:8.7807645797729492,
/segment00000.ts
#EXTINF:7.9985880851745605,
/segment00001.ts
#EXTINF:6.8206477165222168,
/segment00002.ts

.tsファイルを確認しよう

.tsファイルをWindowsにコピーしてダブルクリックで実行します。カメラ映像が再生されれば成功です。(.tsファイルは動画データなんですね、へぇー)

Nodejsをインストールしよう

GStreamerでストリーミングデータが作れるようになりました。そのデータをWebブラウザで開くことができるように、NodejsをインストールしてラズパイをWebサーバにします。

# npmを更新
sudo npm install npm@latest -g
# npmのバージョン確認
$ npm -v
# -> 6.13.4

$ sudo apt update
$ sudo apt install curl
$ curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
$ sudo apt install nodejs -y

# nodejsのバージョン確認
$ node -v
# -> v12.14.0

Nodejsがインストールできたので、プロジェクトを作成します。

$ mkdir nodejs
$ cd nodejs
$ npm init
$ npm install --save express
$ touch app.js
$ mkdir wwwroot
$ touch wwwroot/test.html

app.jsファイルを以下のようにします。

app.js
varexpress=require('express');varapp=express();app.use(express.static('wwwroot'));varport=3000;app.listen(port,function(){console.log("サーバがポート%dで起動しました。モード:%s",port,app.settings.env)});

test.htmlファイルを以下のようにします。

wwwroot/test.html
<!DOCTYPE html><html><head></head><body>
TEST
</body></html>

Webサーバとして動作するか試運転します。

$ pwd
/home/pi/nodejs
 $ node app.js
サーバがポート3000で起動しました。モード:development

ブラウザでhttp://<ラズパイのIPアドレス>:3000/test.htmlを開き、Webサイトが開けば成功です。

ストリーミングサーバにしよう!

ストリーミングデータの作成とWebサーバを用意できました。これら2つを使ってストリーミングサーバを作ります。

具体的には、ストリーミングデータをhtmlの<video> </video>タグで表示できるようにします。そのためには、video.jsというモジュールを使うことでChromeでも表示できるようになります。

必要なファイル(.m3u8, .ts以外)は、githubに置きました。これをwwwroot配下に格納します。
https://github.com/zgw426/GStreamerSample/tree/master/sample01

ストリーミングデータ含め、各ファイルがこのように配置されるようにします。

$ pwd
/home/pi/nodejs/wwwroot
$ tree
.├── output.m3u8
├── segment00056.ts
├── segment00057.ts
├── segment00058.ts
├── segment00059.ts
├── segment00060.ts
├── segment00061.ts
├── segment00062.ts
├── segment00063.ts
├── static
│   ├── css
│   │   └── video-js.min.css
│   └── js
│       ├── video.min.js
│       ├── videojs-contrib-hls.min.js
│       └── videojs-contrib-media-sources.min.js
└── streamtest.html

ストリーミングサイトを開いてみよう

http://<ラズパイのIPアドレス>:3000/streamtest.htmlを開くとこのような画面が表示されます。

動画を再生して、ラズパイに付けたカメラ映像が動画として表示されれば成功です。

これで、ストリーミングサーバが完成しました。

CORSを有効にしよう

冒頭に紹介した2つ目の構成にします。

別にWebサーバーを用意して、そちらのサイトでストリーミング配信ができるようにします。これには、CORS(オリジン間リソース共有)を有効にする必要があります。

CORSが有効でないと、Access to XMLHttpRequest at 'http://xxx' from origin 'http://yyy' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.というエラーが発生し再生できません。

NodejsにCORSモジュールをインストールしよう

ラズパイのNodejsにCORSを有効にするため、モジュールをインストールします。

$ npm install cors

app.jsを以下のようにします。これで、CORS設定が完了です。

※注意※
この設定では、どのサーバからもリソースが利用できる状態で、セキュリティ的には危険な状態です。本来なら、特定のサーバからのみアクセス許可するなど、許可する範囲を最小にします。

app.js
varexpress=require('express');varcors=require('cors');varapp=express();app.use(cors());app.use(express.static('wwwroot'));varport=3000;app.listen(port,function(){console.log("サーバがポート%dで起動しました。モード:%s",port,app.settings.env)});

CORSを有効にすると、このように別PCのWebサーバでもストリーミング配信ができるようになります。

おまけ:遅延テスト

ストリーミング配信にはいくらか遅延があります。配信時間が長くなった場合、遅延がどうなるか検証してみました。16時間連続稼働でもそれほど遅くならず、個人的には満足な値でした。

結果

遅延
開始時18秒
1.5時間後42秒
16時間後27秒

obnizOS 3.0.0でobniz-nobleを使って周囲のBLEデバイスを探してみる

$
0
0

obnizOS 3.0.0がリリースされました。

新バージョンではBLE機能が強化されている印象を受けました。

手持ちのobnizBordで新機能を試してみたい。

Node.jsでobniz-nobleを使ってをobnizで周辺にあるBLEデバイスを検索してみる

npm i obniz-noble
server.js
'use strict';constexpress=require('express');constPORT=process.env.PORT||3000;constapp=express();varobnizNoble=require('obniz-noble')varnoble=obnizNoble("OBNIZ_ID_HERE")noble.startScanning();// any service UUID, no duplicatesnoble.on('discover',function(peripheral){console.log('発見したデバイス: '+peripheral.advertisement.localName);console.log('デバイスのUUID: '+peripheral.advertisement.serviceUuids);console.log();});app.listen(PORT);console.log(`Server running at ${PORT}`);

こんな感じに検索されてくる。
ちょうど良いところにmicrobitが上がってきた。
image.png

とりあえずここまで!
次はNode.jsでobnizとmicrobitを繋げるところの記事を書く予定です。

【ヒーローズ・リーグ 駅すぱあと賞受賞】みんなの現在地から集合場所を決めてくれる機能、他。飲み会幹事おたすけ LINE BOT 「鯨飲くん」を作った

$
0
0

はじめに

飲み会幹事の仕事って、なかなか大変ですよね。
みんな飲み会はやりたいけど、幹事はやりたくない。そんな幹事の負担を少しでも軽減してくれるサービスがあったら、もっと気軽に飲み会を開催できるようになって、みんなの繋がりをより広く、深くできるのではないか…
そんな思いを形にした LINE Bot「鯨飲くん」を作りました。

ProtoPedia はこちら。

https://protopedia.net/node/1899

この作品はヒーローズ・リーグ 2019 のハッカソン予選で制作したものです。ありがたいことに、「駅すぱあと賞」を頂くことができました。
image.png

2日間のハッカソンを通しての制作プロセスや、制作時にやってよかったこと、反省点などを後半でご紹介いたします。

作ったもの

LINE のグループチャットに参加させて利用する LINE Bot です。

  1. みんなの現在地からだいたいの中間地点になる集合駅を提案
  2. 集合駅付近の居酒屋をピックアップ、投票を募る
  3. 一番票の多かった居酒屋に自動音声で電話して予約

以上をワンストップで行います。

1. みんなの現在地を教えてもらう

ユーザの「集合」のかけ声で鯨飲くんが起動します。
参加希望者は鯨飲くんに自分の位置情報を教えます。
鯨飲くんは位置情報を受け取るたび、最寄り駅を探して返答します。
image.png

2. 集合駅を決める

ユーザの「集合場所」のかけ声で参加希望を締め切ります。
鯨飲くんは全員の最寄り駅からの移動時間の合計が最小になる駅を教えてくれます。
image.png

3. 店を提案し、投票を募る

鯨飲くんは集合駅周辺の居酒屋をピックアップし、カルーセルで表示します。
参加者は「ここがいい!」を押して投票することができます。
カルーセル.gif

4. 店を決める

投票の結果、一番票が多かった店に決定します。
image.png

5. 店を予約する

ユーザが「予約しといて!」を押すと、鯨飲くんは予約する時間を尋ねます。
ユーザが時間を回答すると、時間を答えた人の名前で電話予約をはじめてくれます。
image.png

店にはこのような電話がかかってきます。

6. 予約完了を通知する

予約が完了すると、鯨飲くんが予約の成否を通知してくれます。
image.png

あとはみんなで集まって飲むだけですね!

仕組み

構成図

image.png

使ったもの

  • LINE Messaging API
    • いわゆる LINE Bot
    • UI 開発なしで、LINE の仕組みを利用してユーザとやりとりする仕組みを手早く構築可能
  • Express.js
    • LINE Messaging API のバックエンド
    • SDK があるため開発が容易
  • 駅すぱあと Web Service
    • ヒーローズ・リーグのパートナー企業であるヴァル研究所提供
    • 緯度経度からの周辺駅検索
      • 位置情報から最寄り駅を検索するのに利用
    • 範囲探索
      • 各参加者の最寄り駅から集合駅を決定するのに利用
  • Twilio
    • ヒーローズ・リーグのパートナー企業である KDDI 提供
    • 「電話をかける」「自動音声を流す」「番号入力を受け取る」などの様々なアクティビティから構成されるフローを GUI で構築可能
      • image.png
  • ぐるなび Web Service

当日の開発の流れ

アイデアソン

ヒーローズ・リーグの説明、パートナー企業提供サービスの紹介などが済んだあと、アイデアソンが行われました。
各参加者が個別にアイデアスケッチを出し、参加者同士で見せあって、自分のアイデアの実現に協力してくれるチームメンバーを集めます。
ありがたいことに私が出したアイデアに2人が協力を申し出てくれたため、3人でチームを組むことになりました。

  • アイデアスケッチ(再現・当時のものを紛失したため) image.png

開発

あとはひたすら開発です。
以下のようにタスクを分解し、チームメンバーで分担して進めました。

  • LINE Bot のバックエンド開発
  • 各種 Web API を呼び出すモジュールの開発
  • Twilio の電話応答フロー構築
  • ブランディング (鯨飲くんというマスコットの考案)
  • 発表資料の作成

開発を進め、動くものができあがっていく中で、こうするともっといいんじゃない?というアイデアはどんどん出てきます。
そうしたアイデアも時間が許す限り取り込んでいきました。
最初は集合駅を決めるというシンプルなアイデアだったものが、最終的には電話予約まで取ってくれるものに膨らんでいきました。

振り返って

やってよかったこと

デモに必要ないタスクは捨てる

ハッカソンは、デモで印象づけることが命です。
実際のプロダクト開発では、エラー系や異常系、セキュリティなどを考慮して開発を行います。
しかし、デモで見せるのは正常系だけです。限られた時間の中でユーザーにとって魅力的な機能を可能な限り多く実装することを最優先にしました。

具体的には以下のような割り切りをしました。

  • エラー系、異常系は一切実装しない。ユーザーは常に正しい入力をする前提を置く
  • 各種シークレットはソースコードにベタ書き。GitHub のプライベートリポジトリに直コミットして共有
  • 駅すぱあと Web Service 側の制約で、6人以上が参加する場合は集合駅を出せない。でもデモは3人でやるから👉ヨシ!

各メンバーの開発成果物を npm モジュールとして分離する

複数人でソースコードを共有して開発すると、他のメンバーの開発がボトルネックになって開発が進まない、ということが往々にして起きます。
これを防止するために、メンバーごとの開発成果物を npm モジュールとして分離し、LINE Bot のバックエンドから require()して利用してもらいました。

具体的には以下のように開発を進めました。
私が、駅すぱあと Web Service を呼び出して位置情報から最寄り駅を取得する機能を開発していたときのケースです。

  1. 駅すぱあと Web Service を呼び出す npm モジュールに findNearestStationという function を作成する
    • この function は、緯度経度を受け取って最寄り駅を返すものだが、実際に Web API を呼び出す処理の実装は後回しにし、常に「新宿駅」を返すよう実装した状態でコミットする
  2. 他のメンバーには、常に「新宿駅」を返す実装のまま、この function を利用して開発を進めてもらう
  3. 正しく最寄り駅を返すよう実装が終わったら再度コミットする
  4. 他のメンバーは最新のコードを pull するだけで、自らのソースコードに一切変更を加えることなく正しい最寄り駅を受け取ることができる

また、モジュールの使い方を説明するときには、実際にモジュールを利用するサンプルコードを用意して渡すと非常にスムーズでした。
以下は実際に利用したサンプルコードです。
駅すぱあと Web Service を利用してメンバー3人の最寄り駅を取得し、集合駅を決めるところまでを実行しています。

ekispertUsage.js
constekispert=require("ekispertjs");constnomikai=newekispert.Nomikai();constmain=async()=>{// 横浜conststation1=awaitekispert.findNearestStation("35.450199","139.627466");console.log(station1.name);nomikai.currentStations.push(station1.code);// 立川conststation2=awaitekispert.findNearestStation("35.700230","139.413544");console.log(station2.name);nomikai.currentStations.push(station2.code);// 大宮conststation3=awaitekispert.findNearestStation("35.907608","139.631058");console.log(station3.name);nomikai.currentStations.push(station3.code);// 集合場所を探すconstmeetPlace=awaitnomikai.findPlace();console.log(meetPlace.name);console.log(meetPlace.geoPoint);// 各自の所要時間を位置情報を送った順に並べたarrayconsole.log(meetPlace.costs);}main();

ブランディング

今回作ったものが「飲み会幹事お助けBot」という名前だったらどうでしょうか。
確かにわかりやすい名前ですが、印象に残らないし、きっとすぐに忘れられてしまいます。

「鯨飲くん」というマスコットを作成し、また特徴的な口調でキャラクター付けをすることで、一気にキャッチーさが増し、すぐに覚えてもらえるようになりました。
ハッカソンに限らず、実際のプロダクト開発でも非常に大切なことです。

このブランディング作業は、私が何も言っていないにも関わらず、メンバーが率先してやってくれました。
いいチームメンバーに恵まれたことに感謝しています。

反省点

プレゼンの練習をする余裕がなかった

可能な限り多くのアイデアを詰め込むことに前のめりになった結果、成果発表プレゼンの練習をする時間が取れませんでした。結果、プレゼンが少々ぐだりました。
どんなプロダクトでも、プロダクト自体の品質と同じくらい、「どう見せるか」が重要になります。
プロダクトの技術力そのものが「すごい」と言えるほどではなくても、見せ方が非常にうまくて賞をかっさらっていく作品はハッカソンでは珍しくありません。
成果発表プレゼンは少なくとも1回以上練習しておくことをおすすめします。

タッチ & トライの準備不足

デモでは、やはりハードウェアとして動く部分があるものが強いです。
今回のハッカソンでも、プロジェクションマッピングを利用した作品が最も多くの票を集めていました。

一方で、鯨飲くんはソフトウェアで完結しているため、デモのインパクトは弱いです。
しかし、実際にタッチ & トライで触ってもらえればその魅力は伝わります。実際に触った人たちからは口々に「これはすごいね」という評価を頂けました。

ところが、鯨飲くんにはタッチ & トライに至るまでの準備に時間がかかるという欠点があります。
鯨飲くんと友だちになってもらった上で、鯨飲くんのいるLINEグループに参加してもらう必要があるためです。
ここに手間取った結果、会場にいるうちの半分程度の人にしか試してもらうことができませんでした。

タッチ & トライに至るまでの時間で、参加者に声をかけて事前にグループに入ってもらえていたら、もっと多くの人に試してもらうことができたでしょう。
これは鯨飲くんに限った話ではなく、開発途中にでも作品を見てもらうよう周囲の人に積極的に声をかけることはとても効果的です。
早い段階でフィードバックを得て、ブラッシュアップをかけることができるからです。

感想

このハッカソンの中では、自分の中では満足の行くものができつつも、「反省点」に記したように見せ方に失敗し、総合賞に届くことができませんでした。

しかし、これはあくまでも「ハッカソン予選」です。ヒーローズ・リーグでは全国の至るところで予選が開催され、最終的にヒーローズ・リーグ全体での賞が決まります。
その全体賞として、パートナー企業のヴァル研究所様に評価頂き、「駅すぱあと賞」を受賞することができました。
見ていてくれる人はいるんだな、と、報われたような気持ちがして、非常に嬉しかったです。

今後も、「反省点」に記したことを胸に留めつつ、ハッカソンに挑戦していきたいです。
本当にいい経験になりました。

後日談

LINE Bot でお店探しや予約代行をしてくれるぺこったーというサービスがあるそうです。
鯨飲くんの後半の機能と被っている…どころか、こちらのほうが遥かに便利そうですね。
予約の電話ってやはり腰が重いタスクなので、多くの人にとって非常にありがたいサービスではないでしょうか。
image.png

かわいいマスコットを作って、口調でキャラクター付けをしているあたり、まさにブランディングですね。

鯨飲くんでは自動音声での予約を行います。いつかそれが普通になる日が来るのかも知れないですが、現状それをやったらお店の人を驚かせてしまいますよね…。
鯨飲くんで集合駅を決めたあと、ぺこったーに予約をお願いできれば強そうです。
LINE Bot 同士の連携が難しいので、すぐに実装というわけには行きませんが。
LINE さんどうかよろしくお願いします。

Special Thanks

  • パートナー企業として駅すぱあと Web Service を提供してくださり、さらに駅すぱあと賞で鯨飲くんを救ってくれた、ヴァル研究所様
  • パートナー企業として Twilio を提供してくださった、KDDI 様
  • 私のアイデアを形にするために最善の協力をしてくれた、チームメンバーの2人
  • ハッカソンの最中に利用していた画像のぬいぐるみを販売している、ニトリ様
    • 勝手に使ってごめんなさい。宣伝しておきます!
  • ポップでかわいい鯨のイラストを提供してくださった、いらすとや様

StreamでBuffer.concatしない冴えたやりかた

$
0
0

はじめに

この記事は Node.js Advent Calendar 2019の 24 日目です。

https://twitter.com/yosuke_furukawa/status/1201778011286065153
Yosuke FURUKAWA @yosuke_furukawa
なんだこの誕生日アドベントカレンダーは / “Node.js Advent Calendar 2019 - Qiita”
午後5:19 · 2019年12月3日·はてなブックマーク

最初誕生日カレンダーみたいになってたので、ネタ記事書こうかなーと思ってたら案外真面目な記事ばかりで、真面目に書くことにしました。
(そう、私は12/24生まれです!{クリスマス,誕生日}{ケーキ,プレゼント}はいつも一つ。)

StreamでBuffer.concatしない冴えたやりかた

一般的にStreamを扱う時は on("data", ...)で処理することが多いと思います。
しかし、実際に使うサイズと流れてくるサイズは一致しないことが多く…

constnet=require('net');functiononRecv(packet){/* ... */}constserver=net.createServer(conn=>{letreceivedQueue=Buffer.alloc(0);conn.on('data',chunk=>{letdata=Buffer.concat([receivedQueue,chunk]);//40バイトずつ読み出したいwhile(data.length>=40){onRecv(data.slice(0,40));data=data.slice(40);}receivedQueue=data;});}).listen(3000);

のような処理になりがちです。…なんだか冗長に感じませんか?

他の言語では読み出し可能バイト数を取得、指定バイト以上なら一気に読み出す…みたいな処理を書くことが多いと思います。Arduinoのシリアル通信とか。
Node.jsで同じような処理は書けないのか?書けます。

Node.jsには、任意のバイト数を読み出すための readableイベントが存在します。

しかし、以前までは "読み出し可能なバイト数" を取得する方法が
stream._readableState.length
しかありませんでした。_が気になりますね…
でも今は大丈夫!
v9.4.0から、これを取得するための readableLengthプロパティが追加されています。
stream.readableLength
罪悪感(?)が無くなりましたね。

これを使うと、先程のコードを以下のように書くことができます:

constnet=require('net');functiononRecv(packet){/* ... */}constserver=net.createServer(conn=>{conn.on('readable',()=>{while(conn.readableLength>=40){onRecv(conn.read(40));}});}).listen(3000);

ああ…気持ちいい!これが書きたかった。これが求めていたもの・・・
コードが綺麗になることは勿論、Buffer.concatはデータの複製を行うので、処理速度の改善も期待できます。
このパターンが使える場面では積極的にBuffer.concatを排除していきましょう。

注意点

この方法で読み込める最大バイト数は highWaterMarkまでになります。
大きなデータをまとめて取得したい時は、 highWaterMarkの上限値を拡大する必要があります。
※ただ、デフォルトの highWaterMarkを超えるデータを一気に処理する処理自体あまり良くないので、少しずつ処理できるようにしたほうがいいかもしれません。

p.s. highWaterMarkは、シンプルに説明すると他言語のbufferSizeみたいなものです。(実際にはもっとイケてる実装の一部だったりしますが、とりあえずはそんなものだと思えば十分。)

さいごに

あまりreadableイベントを使った記事を見かけないので書いてみました。お試しあれ。
おわりー。

Alexa+lambdaでスマート(じゃない)ホームスキルをつくる

$
0
0

はじめまして@ufoo68です。今回はAWSのアドベントカレンダーにお邪魔させていただきました。

はじめに

このアドベントカレンダーに登録したきっかけは最近re:invent2019に参加した経験からです。普段はあまりAWS関係の記事は書いたりしないのですが、せっかく大きなイベントに参加したのでlambdaについて書いてみよと思った次第です。イベントの発表を聴いて気に入ったサービスについてはこの記事でまとめました。今回はその中のAlexaに挑戦してみようと思い、思い切ってEcho Show 5を購入してみました。

が、到着予定日がまさかの1/17~1/23とかいうことになってしまい(サイバーマンデーでポチったのでこういうこにになったのか?)、Alexa開発がアドベントカレンダーに間に合わないという事態に!!というわけで、今回はシミュレーターとかを使ってスキルを作ってみようかなと思います。まあ、lambda+Alexaでの今後の開発のための練習ということにしておきましょうw

スキルについて

今回つくるのは「スマート(じゃない)ホームスキル」です。本当はスマートホーム的なのやりたかったわけですが、何せ実機がないものですからこういうものを考えました。これは何かというと、スマートホームのフリをして返事だけは一丁前にするスキル、つまり「ライトをつけて」とかいうと「わかりました」と返事だけはするが実際には何もしてくれない、そんなスマートホームもどきのスキルをつくってみようと思います。

スキルの実装

今回はこちらを参考にしてCodeStarを用いてコードを実装やらデプロイを行いました。これを用いると対話モデルの作成とか全部をAWSでやってくれるみたいです。開発は「Hallo World Skill」というサンプルテンプレートを用いて開発しました。ここで公開しています。

まずは、skill.jsonを以下のようにしました。

skill.json
{"manifest":{"publishingInformation":{"locales":{"ja-JP":{"summary":"スマートじゃない、スマートホームスキル","examplePhrases":["Alexa、スマートじゃないホームを起動して","電気をつけて","電気をけして"],"name":"スマート(じゃない)ホーム","description":"スマートじゃない、スマートホームスキル"}},"isAvailableWorldwide":true,"testingInstructions":"Sample Testing Instructions.","category":"EDUCATION_AND_REFERENCE","distributionCountries":[]},"apis":{"custom":{}},"manifestVersion":"1.0"}}

あとは対話モデルは以下のように設定しました。

interactionModels\custom\ja-JP.json
{"interactionModel":{"languageModel":{"invocationName":"スマートじゃないホーム","intents":[{"name":"AMAZON.CancelIntent","samples":[]},{"name":"AMAZON.HelpIntent","samples":[]},{"name":"AMAZON.StopIntent","samples":[]},{"name":"LightOnIntent","slots":[],"samples":["電気をつけて","ライトをつけて"]},{"name":"LightOffIntent","slots":[],"samples":["電気を消して","ライトを消して"]}],"types":[]}}}

コードもあまりサンプルと変わってませんが、こんな感じです。

lambda\custom\index.js
// This sample demonstrates handling intents from an Alexa skill using the Alexa Skills Kit SDK (v2).// Please visit https://alexa.design/cookbook for additional examples on implementing slots, dialog management,// session persistence, api calls, and more.constAlexa=require('ask-sdk-core');constLaunchRequestHandler={canHandle(handlerInput){returnhandlerInput.requestEnvelope.request.type==='LaunchRequest';},handle(handlerInput){constspeechText='スマートじゃないホームへようこそ。';returnhandlerInput.responseBuilder.speak(speechText).reprompt(speechText).getResponse();}};constLightOnIntentHandler={canHandle(handlerInput){returnhandlerInput.requestEnvelope.request.type==='IntentRequest'&&handlerInput.requestEnvelope.request.intent.name==='LightOnIntent';},handle(handlerInput){constspeechText='はい、つけます。';returnhandlerInput.responseBuilder.speak(speechText)//.reprompt('add a reprompt if you want to keep the session open for the user to respond').getResponse();}};constLightOffIntentHandler={canHandle(handlerInput){returnhandlerInput.requestEnvelope.request.type==='IntentRequest'&&handlerInput.requestEnvelope.request.intent.name==='LightOffIntent';},handle(handlerInput){constspeechText='はい、消します。';returnhandlerInput.responseBuilder.speak(speechText)//.reprompt('add a reprompt if you want to keep the session open for the user to respond').getResponse();}};constCancelAndStopIntentHandler={canHandle(handlerInput){returnhandlerInput.requestEnvelope.request.type==='IntentRequest'&&(handlerInput.requestEnvelope.request.intent.name==='AMAZON.CancelIntent'||handlerInput.requestEnvelope.request.intent.name==='AMAZON.StopIntent');},handle(handlerInput){constspeechText='さようなら!';returnhandlerInput.responseBuilder.speak(speechText).getResponse();}};constSessionEndedRequestHandler={canHandle(handlerInput){returnhandlerInput.requestEnvelope.request.type==='SessionEndedRequest';},handle(handlerInput){// Any cleanup logic goes here.returnhandlerInput.responseBuilder.getResponse();}};// Generic error handling to capture any syntax or routing errors. If you receive an error// stating the request handler chain is not found, you have not implemented a handler for// the intent being invoked or included it in the skill builder below.constErrorHandler={canHandle(){returntrue;},handle(handlerInput,error){console.log(`~~~~ Error handled: ${error.message}`);constspeechText=`すいません、聞き取れませんでした。`;returnhandlerInput.responseBuilder.speak(speechText).reprompt(speechText).getResponse();}};// This handler acts as the entry point for your skill, routing all request and response// payloads to the handlers above. Make sure any new handlers or interceptors you've// defined are included below. The order matters - they're processed top to bottom.exports.handler=Alexa.SkillBuilders.custom().addRequestHandlers(LaunchRequestHandler,LightOnIntentHandler,LightOffIntentHandler,CancelAndStopIntentHandler,SessionEndedRequestHandler)// make sure IntentReflectorHandler is last so it doesn't override your custom intent handlers.addErrorHandlers(ErrorHandler).lambda();

動作のようす

シミュレーターで動かしてこんな感じでちゃんと動いてくれました。

slill.png

まあ、実機もないしスマートホームに対応させてないので返事だけなのですが。。。

さいごに

とりあえず実機が届いたらもう少し色々やってみたいですね。

ゲームにおけるFirebase 活用例

$
0
0

PONOS Advent Calendar 2019の24日目の記事です。
🎄メリークリスマス!!クリスマス・イブにFirebaseの記事をお届けします!!!🎄
🎁🎁🎁🎁🎁🎁🎁🎁🎁🎁🎁🎁🎁🎁🎁🎁🎁🎁🎁🎁🎁🎁🎁🎁🎁🎁🎁

はじめに:eyeglasses:

現在運用中のゲームでFirebaseを導入しました。
この記事では活用事例とノウハウを紹介していきます。
Firebase利用の一例になれば幸いです。

組み込み方法は別記事をご参照ください
公式ドキュメント
iOS
Android
Unity
C++
Web

この記事の対象者:eyeglasses:

・Firebase初心者、中級者
・Firebaseを検討中の方
・Firebaseの実装イメージを掴みたい方
・サーバーレスを検討中の方
・Googleのサービスが好きな方、興味のある方

Firebaseの各機能をざっくり紹介

機能がたくさんあります

名称機能活用
Firebase Authentication認証機能
Firebase Realtime DatabaseNoSQL クラウド データベースでデータの保管と同期を行うことができる
Cloud FirestoreRealtime Database からさらにパワーアップした NoSQL データベース。
Cloud Storage for Firebase写真や動画などのコンテンツを保管、取得することができる。
Firebase Hostingwebサイトを構築
Cloud Functions for Firebase関数ごとにサーバー処理を簡単に用意できる
ML Kit for Firebase機械学習×
Firebase Crashlyticsクラッシュ レポート管理
Firebase Performance Monitoringパフォーマンスの問題を診断
Firebase Test Lab様々なデバイスでアプリの自動テストおよびカスタマイズされたテストを実行
Firebase Cloud Messagingサーバから通知機能
Firebase In-App Messagingターゲットを絞り込んでアプリ内で通知×
Firebase Predictions機械学習を用いた分析ツール
Firebase Remote ConfigFirebase consoleから、外観の変更、機能の段階的な展開などをカスタマイズ×
Google Analytics for Firebase総合的な分析ツール
Firebase A/B Testingユーザーごとに出し分けしてA/Bテストを行う
Firebase Dynamic Links複数のプラットフォームで機能するURLを生成×
Firebase App Indexingアプリを Google 検索結果に表示することができる×

活用1:ログイン時のユーザー認証

Firebase Authenticationをゲーム内での認証機能として使っています。
ログイン時に認証して、Firebase各種機能と連携して認証済みのアカウントしかセキュリティー的に許可しないようにしています。
以下の4種類の認証を使っています。
・Google
・Facebook
・メールアドレス
・匿名認証

匿名認証で作成したデータと各種連携したデータを紐づける処理はこちらで実装する必要がありますが、
公式ドキュメントにもサンプルコードが用意されているので低コストで実装できます。

匿名認証のコード

Auth.cpp
#include "firebase/app.h"
#include "firebase/auth.h"
voidAuth::signInAnonymously(){firebase::App*app=App::GetInstance();firebase::auth::Auth*auth=Auth::GetAuth(app);auth->SignInAnonymously().OnCompletion([this](constfirebase::Future<firebase::auth::User*>&result){if(result.status()==firebase::kFutureStatusComplete&&result.error()==firebase::auth::kAuthErrorNone&&result.result()){//成功処理}else{//失敗処理}});}

活用2:通信時の不正対策

通信時の不正対策として
Firebase Authentication

Cloud Functions
を組み合わせてサーバーサイドでも認証チェックしております。

Cloud Functionsを使用するとクライアント側の通信処理もセキュアでシンプルな作りになります。
詳細はCloud Functionsのドキュメントを一読するのもありです。

クライアント側の通信処理
Cloud Functionsで定義したsampleにリクエストを投げます。

Request.cpp
voidRequest::callSample(){firebase::Variantdata=firebase::Variant::EmptyMap();firebase::functions::HttpsCallableReferencedoSomething=functions->GetHttpsCallable("sample");doSomething.Call(data).OnCompletion([this](firebase::Future<firebase::functions::HttpsCallableResult>result){if(result.status()==firebase::kFutureStatusComplete&&result.error()==firebase::functions::kErrorNone&&result.result()){//成功処理}else{//失敗処理}});}

サーバー側の認証チェック
exports.sampleの部分がレスポンスを返します。

Server.js
constfunctions=require('firebase-functions');constadmin=require('firebase-admin');admin.initializeApp(); exports.sample=functions.https.onCall((data,context)=>{if(!checkAuth(data,context)){thrownewfunctions.https.HttpsError('failed-precondition','The function must be called '+'while authenticated.');}//以降でサーバー処理を記入していく return{text:"success",};});//認証チェックasyncfunctioncheckAuth(data,context){if(!(context.auth&&context.auth.uid&&context.auth.token)){console.log('checkAuth error');returnfalse;}constuser=awaitadmin.auth().getUser(context.auth.uid);if(user.customClaims&&user.customClaims.admin!==true){console.log('checkAuth error');returnfalse;}returntrue;}

活用3:サーバーからのPush通知

Cloud Messagingを活用して特定条件を満たしたユーザーにPush通知を送るようにしています。
このサンプルソースではRealTimeDBにユーザーのpushTokenが保存されており、pushTokenを取り出して
使用しているパターンです。

Server.js
exports.pushNotification=functions.https.onCall((data,context)=>{if(!checkAuth(data,context)){thrownewfunctions.https.HttpsError('failed-precondition','The function must be called '+'while authenticated.');}constuserId=data.userId;varpass="user_table/"+userId+"/"+"pushToken";returnadmin.database().ref(pass).once('value').then(function(snapshot){varpushToken=snapshot.val();constpayload={notification:{title:'プッシュ通知タイトル',body:`プッシュ通知本文`}};returnadmin.messaging().sendToDevice(pushToken,payload);}).catch(error=>{console.error(error);thrownewfunctions.https.HttpsError(error);});});

シンプルに書くとこれだけで通知を送れます。

          return admin.messaging().sendToDevice(pushToken, payload);

活用4:バトルログの保存(アップロード)

バトル時のログや詳細情報はCloud Storageに保存しています。
DBに保存すると量と値段の面で、デメリットがあったので分析の必要がなく、他のユーザーから参照されないデータはCloud StorageにGZip化して保存しています。

CloudStorage.cpp
#include "firebase/app.h"
#include "firebase/storage.h"
voidCloudStorage::storageUpLoad(){firebase::App*app=App::GetInstance();firebase::storage::Storage*storage=Storage::GetInstance(app);//アップロード先のパスstd::stringserverPath="/";//アップロードするファイル保存先std::stringlocalPath="file://"+"ローカルパス";storage->GetReference().Child(serverPath).PutFile(localPath.c_str()).OnCompletion([](constfirebase::FutureBase&result){if(result.error()==firebase::storage::kErrorNone){//成功処理}else{//失敗処理}});}

活用5:リソースダウンロード

これもCloud Storageを活用しています。
リソース数、容量などによって状況は変わると思いますが、問題ない速度でダウンロードできています。
何よりもCloud Storageはコストパフォーマンスがいいです。
現在、ほぼ無料枠で済んでいます。

CloudStorage.cpp
#include "firebase/app.h"
#include "firebase/storage.h"
voidCloudStorage::storageDownload(){firebase::App*app=App::GetInstance();firebase::storage::Storage*storage=Storage::GetInstance(app);//対象リソースのパスstd::stringserverPath="/";//ダウンロード後の保存先std::stringlocalPath="file://"+"ローカルパス";storage->GetReference().Child(serverPath).GetFile(localPath.c_str()).OnCompletion([](constfirebase::FutureBase&result){if(result.error()==firebase::storage::kErrorNone){//成功処理}else{//失敗処理}});}

活用6:ゲーム内お知らせ

お知らせ機能はFirebase Hostingを活用しています。
環境構築などの導入は非常に簡単です。
5分くらいでできると思います。
環境構築

環境構築が終わったら、HTML,CSS,画像などのWEB素材を用意して

firebase deploy

で、デプロイして完了です。

固定のお知らせやヘルプページを見せるだけでしたらFirebase Hostingだけで十分なのですが
ユーザーごとに見せるページを変えたい、期間で出し分けをしたい
などの仕様によってはサーバー側でHTMLを生成する必要があるので
うちのゲームではCloud Function、RealTimeDatabaseなども絡めて使っている箇所があります。

注意点としては
過去にデプロイしたWEBページがCDNに溜まっていき、容量が無駄になるので
Firebaseコンソール上からときどき消去しないといけません。
WEBページに問題があったときに、とっさにロールバックできるのでそれとトレードオフという感じですね

活用7:KPI 分析

ゲーム運営にKPI 分析は必須ですよね
Google Analytics for Firebaseを使っています。
for Firebaseと書いていますが、バックエンドの仕組みはGoogle AnalyticsでKPI分析するうえでは違いはありません。違いは組み込み方法が若干違うくらいです。

スクリーンショット 2019-12-19 18.55.43.png

こちらのFirebase デモプロジェクトを触ってみるのが理解が早いです。

1.デモプロジェクトログイン
デモプロジェクトが見れるようになったらAnalyticsページへ
2.デモプロジェクト Analyticsページ
※リンクから飛べない場合は、左側のメニューにてアナリティクス配下のDashboardをクリック

Analyticsは組み込むだけでセッション数、DAU、課金額、ARPPUなど
デフォルトでいくつかの値を集計してくれます。
ゲーム内独自の集計をしたい場合は、イベントを実装すればできます。

イベント実装例

FirebaseAnalytics.cpp
#include "firebase/app.h"
#include "firebase/analytics.h"
voidFirebaseAnalytics::init(){firebase::App*app=App::GetInstance();firebase::analytics::Initialize(*app);}//レベルアップ時、イベント登録voidFirebaseAnalytics::levelUp(){firebase::analytics::LogEvent(firebase::analytics::kEventLevelUp);}

活用8:予測データの運営活用

Firebase PredictionsではGoogleの機械学習の予測結果を簡単に知ることができます。

スクリーンショット 2019-12-19 19.03.49.png

デモプロジェクト

以下項目の一週間後の予測結果を知ることができます。
churn離脱ユーザーの数

not_churn継続ユーザーの数

spend課金するユーザーの数

not_spend課金しないユーザーの数

Firebase Predictionsの優れている点は
これらの予測結果に対して
・A/Bテストを行なって動向を分析できる
・Remote ConfigによってUIやパラメーターなどを瞬時に変えることができる
・Cloud Messagingを使ってお得な情報などを送ることができる
ことにあると思います。

また、Google Analyticsと連携してコンバージョンに設定されたイベントを予測項目として追加することもできます。
ゲームごとに有効なイベントを設定すれば有効活用できると思います。

活用9:バナー広告、動画広告

Google AdMobを使って、バナー広告、動画広告に対応することができます。
Firebaseの機能としてあまり注目されてないですが、実はFirebase SDKではAdMobの機能もサポートされてます。

バナー広告表示

FirebaseAdmob.cpp
#include "firebase/app.h"
#include "firebase/admob.h"
#include "firebase/admob/banner_view.h"
voidFirebaseAdmob::createBanner(){//firebase::admob::BannerView* banner_viewbanner_view=newfirebase::admob::BannerView();firebase::admob::AdSizead_size;ad_size.ad_size_type=firebase::admob::kAdSizeStandard;ad_size.width=320;ad_size.height=50;// my_ad_parent is a reference to an iOS UIView or an Android Activity.// This is the parent UIView or Activity of the banner view.// kBannerAdUnitはAdMobのサイトで発行されるIdbanner_view->Initialize(getAdParent(),kBannerAdUnit,ad_size);}voidFirebaseAdmob::showBanner(){banner_view->MoveTo(banner_view->kPositionBottomLeft);banner_view->Show();firebase::admob::AdRequestmy_ad_request={};banner_view->LoadAd(my_ad_request);}

活用10:多端末テスト

スマホゲーム開発において多端末検証って大変ですよね
特にAndroidとかAndroidとかAndroidとか・・・
そこで活躍するのが
Firebase Test Lab
Google データセンターでホストされているデバイス上でアプリをテストします。
1 回のオペレーションで、さまざまなデバイス、
さまざまなデバイス構成で Android アプリや iOS アプリをテストし、
Firebase consoleで結果(ログ、動画、スクリーンショットなど)
を確認できます。

・Android端末が足りない場合
・テスターが足りない場合
・QA開始前のシステム的なテストをしたい場合
など使いどころはたくさんあります。

スクリーンショット 2019-12-19 19.04.49.png

1.デモプロジェクトログイン
デモプロジェクトが見れるようになったらTest Labページへ
2.デモプロジェクト Test Labページ
※リンクから飛べない場合は、左側のメニューにて品質、配下のTest Labをクリック

おわりに

Firebaseを導入してみての感想としては・・・

非常に簡単、便利で素晴らしいです!
ゲーム仕様や料金面での検討をする必要はありますが
まずは無料プランで使ってみてから他のサービスと比較検討する方法がオススメです!

料金面にて実装で創意工夫した箇所もありますが、結果として今は超・低コストで運営できています。

以下の機能は無料で使い続けることができるので、組み込んでおいて損はないです
・Google Analytics for Firebase
・Firebase Authentication
・Firebase Crashlytics
・Firebase Performance Monitoring
・Firebase Cloud Messaging
・Firebase Predictions
・Firebase Dynamic Links
・Firebase App Indexing
・Firebase A/B Testing

本プロジェクトではGCPも併用して活用しています。
次回はGCPのゲーム活用事例を書きたい思います。

明日は@honeniqさんの記事です
最終日の記事も楽しみですね!

expressメモ

$
0
0

mongooseでdb接続

connect成功と失敗でlogを出力させた。

// Set up Default mongoose connection
var mongoDB = require('./.db_url');
var DB_URL = mongoDB.LOCAL_DB;
// connect mongoDB
mongoose
  .connect(DB_URL, {
    useUnifiedTopology: true,
    useNewUrlParser: true,
  })
  .then(() => console.log('DB Connected!'))
  .catch(err => {
    console.log(`DB Connection Error:${err.message}`);
  });

Hubotを使ったslack botの作り方【2020年版】

$
0
0

Hubotとは

HubotとはGithub社が開発したNode.js上で動くBotアプリケーションを作成するためのフレームワークです。

Hubotは、Hubot本体アダプター拡張スクリプトから構成されます。
Hubot本体とチャットアプリの間を仲介するアダプターが存在するため、slack、chatworkなどの様々なチャットツールとHubotを連携することが可能です。
Bot作成フレームワークはHubot以外にも存在しますが、Hubotは様々なチャットツールと連携できる点が他のものと比べて大きな特徴です。

TH800_001.png
GitHub社謹製! bot開発・実行フレームワーク「Hubot」より引用

Hubotを使うことにより、複雑な振る舞いをするBotを作成することが可能です。
slackの既存機能だけでもslack上で"hello"と発言すれば、Botが"hi"と返すような単純な振る舞いであれば可能ですが、それ以上複雑になると対応できません。

Hubotを使えば、
・決められた時刻に発言
・あるサイトから内容をスクレイピングで取得してチャットツール上に表示
・ある発言をするとapiを経由してGoogleカレンダーに予定を登録
が可能になります。

やること

  1. Hubotアプリケーションの作成
  2. Hubot Integrationの設定してHubotとslackを連携
  3. Heroku上でHubotアプリケーションを動かす

1.Hubotアプリケーションの作成

Hubotアプリケーションを新規作成

node.jsとnpmが入っている確認

node -v
npm -v

yoemanとhubotジェネレータをインストールします。
yoとはyoeman(ヨーマン)と呼ばれるクライアント側のwebアプリケーション作成ツールです。
yoemanを使ってhubotアプリケーションを作成します。

npm install -g yo generator-hubot
mkdir myhubot
cd myhubot
yo hubot

yo hubotを実行すると以下のものがターミナル上に表示されます。
スクリーンショット 2019-12-24 12.54.21.png

4つ質問がされるので以下の通り答えていきます。

Owner→Enter
Bot name→Enter
Description→Enter
Bot adapter→今回はslackと連携したbotを作るので「slack」と入力

これでHubotアプリケーションの雛形が作成できました。

注意
別の記事ではhubot --create myhubotを使ったhubotアプリケーションの作成を紹介しているが、実際にコマンドを実行すると以下のエラーが発生する。
'hubot --create'は非推奨になったみたい。
指示通りにyoemanを使ってhubotを作成する。

'hubot --create' is deprecated. Use the yeoman generator instead:
npm install -g yo generator-hubot
mkdir -p myhubot
cd myhubot
yo hubot
See https://github.com/github/hubot/blob/master/docs/index.md for more details on getting started.

Hubotをローカルで起動

Hubotをローカルで起動して、動作確認をしていきます。

pwd #mybotディレクトリにいることを確認
bin/hubot

bin/hubotはmyhubotディレクトリ内にあるシェルスクリプトhubotを実行しています。
bin/hubotでは、npm installpackage.jsonに記述されたパッケージをインストールし、その後Hubot本体を起動しています。

hubot起動時に使用するアダプターを明示していないと、対話モードでHubotが起動します。
スクリーンショット 2019-12-24 13.06.04.png

まだ独自のスクリプトを作成していませんが、Hubotではデフォルトでスクリプトが登録されています。
mybubot echo "ping"と打って、"ping"と返ってくることを確認しましょう。

mybubot> mybubot echo "ping"
mybubot> "ping"

Hubotの独自スクリプト作成

helloと打つとhiを返してくる独自のBotスクリプトを作成してみましょう。

scripts/hello.coffee
module.exports=(robot)->robot.hear(/hello/i,(res)->res.reply("hi"))

Robotクラスのhearメソッドは、hear(マッチさせたい正規表現, コールバック関数)
Responseクラスのreplyメソッドは、reply(発言内容)

robot.hear(/hello/i, (res)で"hello"という文字列が渡ってくると、第2引数のコールバック関数を実行します。res.reply("hi")で"hi"という文字列をチャットツール側で発言するようにしています。

2.Hubot Integrationの設定してHubotとslackを連携

次はローカルで動くHubotとslackを連携して、slack上にbotを導入してみましょう。

Hubot Integrationの設定

slackのアプリ連携からHubotを追加します。
スクリーンショット 2019-12-24 13.21.28.png

slackのアプリディレクトリからHubotを選択して、HubotのBotユーザを作成します。
ここで入力されたユーザ名が、slack上で発言するBotのユーザ名になります。
スクリーンショット 2019-12-23 12.28.41.png

HubotアダプターのAPIトークンが表示されるので、メモしておきます。

Hubot___Slack_App_ディレクトリ.jpg

アダプターを指定してHubotを起動

slackと連携したHubotをローカル上に起動するために、以下コマンドでHubotを起動します。

HUBOT_SLACK_TOKEN=上記でメモしたHubotアダプターのAPIトークン ./bin/hubot --adapter slack

slack上でhelloと発言するとBotがhiと返してくれるようになりました。

スクリーンショット 2019-12-24 13.31.00.png

ちなみに、slackチャンネル上でBotを動かすには、チャンネルにBotを/invite @ユーザ名で招待しておく必要があります。

3.Heroku上でHubotアプリケーションを動かす

今まではローカル上でHubotを起動していました。
しかしこれだと、常にローカルPCを起動させておく必要がありますし、もしローカルのPCを閉じるとBotが動かなくなります。
そこでHeroku上でHubotを動かしておくことにします。

Herokuに登録

Herokuの登録自体は多くの記事がありますので、ここでは説明を省略します。
以下などの記事を参考に3まで完了させてください。
【Rails】Herokuの登録&初期設定からデプロイ方法までまとめ

1.Herokuのアカウント登録
2.Toolbeltインストール
3.heroku login

Herokuに環境変数設定&Push

Herokuに環境変数HUBOT_SLACK_TOKENを設定します。

heroku config:set HUBOT_SLACK_TOKEN=hoge

Hubotアプリケーションのcommit

git init
git add .
git commit -m "first commit"
git push heroku master  #herokuにmasterブランチをpushする

するとBuild failedエラーが発生。

% git push heroku master                                                          
Total 0 (delta 0), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Node.js app detected
remote:
remote: -----> Creating runtime environment
remote:
remote:        NPM_CONFIG_LOGLEVEL=error
remote:        NODE_ENV=production
remote:        NODE_MODULES_CACHE=true
remote:        NODE_VERBOSE=false
remote:
remote: -----> Installing binaries
remote:        engines.node (package.json):  0.10.x
remote:        engines.npm (package.json):   unspecified (use default)
remote:
remote:        Resolving node version 0.10.x...
remote:        Downloading and installing node 0.10.48...
remote:        Detected package-lock.json: defaulting npm to version 5.x.x
remote:        Bootstrapping npm 5.x.x (replacing 2.15.1)...
remote:        npm 5.x.x installed
remote:
remote: /tmp/build_9e550049100749e72c047c136d3131ae/.heroku/node/lib/node_modules/npm/bin/npm-cli.js:79
remote:       var notifier = require('update-notifier')({pkg})
remote:                                                     ^
remote: SyntaxError: Unexpected token }
remote:     at Module._compile (module.js:439:25)
remote:     at Object.Module._extensions..js (module.js:474:10)
remote:     at Module.load (module.js:356:32)
remote:     at Function.Module._load (module.js:312:12)
remote:     at Function.Module.runMain (module.js:497:10)
remote:     at startup (node.js:119:16)
remote:     at node.js:945:3
remote:
remote: /tmp/build_9e550049100749e72c047c136d3131ae/.heroku/node/lib/node_modules/npm/bin/npm-cli.js:79
remote:       var notifier = require('update-notifier')({pkg})
remote:                                                     ^
remote: SyntaxError: Unexpected token }
remote:     at Module._compile (module.js:439:25)
remote:     at Object.Module._extensions..js (module.js:474:10)
remote:     at Module.load (module.js:356:32)
remote:     at Function.Module._load (module.js:312:12)
remote:     at Function.Module.runMain (module.js:497:10)
remote:     at startup (node.js:119:16)
remote:     at node.js:945:3
remote:
remote: /tmp/build_9e550049100749e72c047c136d3131ae/.heroku/node/lib/node_modules/npm/bin/npm-cli.js:79
remote:       var notifier = require('update-notifier')({pkg})
remote:                                                     ^
remote: SyntaxError: Unexpected token }
remote:     at Module._compile (module.js:439:25)
remote:     at Object.Module._extensions..js (module.js:474:10)
remote:     at Module.load (module.js:356:32)
remote:     at Function.Module._load (module.js:312:12)
remote:     at Function.Module.runMain (module.js:497:10)
remote:     at startup (node.js:119:16)
remote:     at node.js:945:3
remote:
remote: -----> Build failed
remote:
remote:        We're sorry this build is failing! You can troubleshoot common issues here:
remote:        https://devcenter.heroku.com/articles/troubleshooting-node-deploys
remote:
remote:        If you're stuck, please submit a ticket so we can help:
remote:        https://help.heroku.com/
remote:
remote:        Love,
remote:        Heroku
remote:
remote:  !     Push rejected, failed to compile Node.js app.
remote:
remote:  !     Push failed
remote: Verifying deploy...
remote:
remote: !   Push rejected to rocky-gorge-05187.
remote:
To https://git.heroku.com/rocky-gorge-05187.git
 ! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to 'https://git.heroku.com/rocky-gorge-05187.git'

You can troubleshoot common issues here: https://devcenter.heroku.com/articles/troubleshooting-node-deploysこのURL見れば同じ問題を解決できるよーと言われているので、見てみる。

まずはbuildpackを設定して再実行。build failedは解消できず。

heroku buildpacks:set heroku/nodejs

次に章のCompare Node and npm Versionsでは開発環境と本番環境でNodeとnpmのversionを揃えなさいと言われているので、まずはローカルでnodeとnpmのバージョンを見る。

node --version
npm --version

表示されたバージョンをpackage.jsonに設定。
nodeとnpmのバージョンを指定。

package.json
"engines":{"node":"10.16.0","npm":"6.9.0"}

再度、git push heroku masterでbuild成功。

参考文献

GitHub社謹製! bot開発・実行フレームワーク「Hubot」
https://gihyo.jp/dev/serial/01/hubot/0001

Hubotを使ってSlackへBotを投げる
https://qiita.com/shosho/items/057d7b67d1dd3a700554

Viewing all 8843 articles
Browse latest View live