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

SkyWayを使ってビデオチャットアプリを作成してHerokuでデプロイする方法

$
0
0

プログラミング勉強日記

2021年3月19日
SkyWayでビデオチャットアプリを作る方法はいくつかのサイトに載っていたが、Herokuでデプロイするのが大変だったので、記事に残しておく。

開発環境と前提条件

  • Windows10
  • Node.jsが入ってる
  • npmコマンドが使える
  • Herokuのアカウント登録がある

ビデオチャットアプリを作る

1. SkyWayの登録・ログインする

 こちらから無料で始めるをクリックして、会員登録をする。

image.png

2. SkyWayでアプリケーションを作成する

 アプリケーション説明文にvideochat-sample, 利用可能ドメイン名にlocalhostvideochat-sample.herokuapp.comを入力する。

image.png

image.png
image.png

アプリを作成するとAPIキーを取得できる。

image.png

3. GitHubからサンプルコードを入手する

 SkyWayで作成したアプリケーション説明文と同じ名前のフォルダを作る。
 SkyWayの公式のGitHubからサンプルコードを取得し、作ったフォルダに入れる。

image.png

image.png

4. ローカルで動かす方法

 _shared/key.jsの中身をSkyWayで取得したAPIキーを入力する。

_shared/key.js
window.__SKYWAY_KEY__='<YOUR_KEY_HERE>';

 コードを保存してるディレクトリの中でローカルサーバを立ち上げる。

$ ruby -run -e httpd . -p 8080

image.png

 ブラウザでlocalhost:8080にアクセスすると、以下のようにビデオチャットが作成されてる。
image.png

Herokuを使ってデプロイする方法

1. expressをインストールする

 ディレクトリの中で下記コマンドを入力

$ npm init 
$ npm install express

 色々聞かれるが全てEnterで問題ない。expressのインストールが終了すると、package.jsonpackage-lock.jsonが作成される。
image.png

package.json
{
  "name": "videochat-sample",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node app.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "dotenv": "^8.2.0",
    "express": "^4.15.3"
  }
}

2. app.jsファイルを作成する

 ディレクトリ直下にapp.jsファイルを作成する。

videochat-sample/app.js
varexpress=require('express');varapp=express();app.use(express.static('public',{hidden:true}));app.listen(process.env.PORT||8080);

3. フォルダを整理する

 ページにアクセスしたときにすぐにチャット画面が開くように、ディレクトリの中を整理する。publicフォルダを作成し、_shared, p2p-data, p2p-media, roomをpublicの中に入れる。roomフォルダの中にあるindex.htmlscript.jsをpublic直下に置く。
 文章だとわかりにくいので、以下の写真のようになるようにする。

image.png

 階層を変えたので、public/index.htmlのパスを変更する。

public/index.html
<!DOCTYPE html><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1"><title>SkyWay - Room example</title><linkrel="stylesheet"href="_shared/style.css"></head><body><divclass="container"><h1class="heading">Room example</h1><pclass="note">
        Change Room mode (before join in a room):
        <ahref="#">mesh</a> / <ahref="#sfu">sfu</a></p><divclass="room"><div><videoid="js-local-stream"></video><spanid="js-room-mode"></span>:
          <inputtype="text"placeholder="Room Name"id="js-room-id"><buttonid="js-join-trigger">Join</button><buttonid="js-leave-trigger">Leave</button></div><divclass="remote-streams"id="js-remote-streams"></div><div><preclass="messages"id="js-messages"></pre><inputtype="text"id="js-local-text"><buttonid="js-send-trigger">Send</button></div></div><pclass="meta"id="js-meta"></p></div><script src="//cdn.webrtc.ecl.ntt.com/skyway-4.4.0.js"></script><script src="_shared/key.js"></script><script src="script.js"></script></body></html>

 ここで再度ローカルで動くか確認してみてください。

ローカルで動かす
node app.js

4. Herokuにログインする

 Git Bashで以下のコマンドを入力すると、ブラウザが開くのでログインする。

$ heroku login

image.png

5. Herokuでアプリを作成する

 今までと同じ名前でアプリを作成する。

Gitbash
$ heroku create videochat-sample

6. Herokuにデプロイする

$ git init
$ git add .
$ git commit -m "create app"
$ git remote add heroku https://git.heroku.com/videochat-sample.git
$ git push heroku master

 https://videochat-sample.herokuapp.com/にアクセスすると表示される。このとき、httpssを入れることを忘れないように!

image.png

最後に

 4日間かけてアプリを作成してデプロイしたのをまとめた。アプリ名(今回ならvideochat-sample)を全て統一することが重要なので、スペルミス等注意が必要。
 ただ、この方法だと誰でもAPIキーを見れてしまうので、環境変数に入れてうまく隠す必要がある。APIキーを隠す方法もできたら記事に書こうと思う。

 もう1度手順を踏んでやっていたがエラーがあって何度かやり直したので、これだけではいけないかもしれないです…何かありましたらコメントください!
 最後まで読んでいただき、ありがとうございました。

参考文献

【爆速!】5分でビデオチャットを構築する
WebRTC Platform SkyWay
SkyWayを使って多人数ビデオチャットを作ってみた


dotenv使うときに便利!VSCodeで全部大文字の定数名とかのメンドーなやつ解決する拡張機能

$
0
0

これです

change-case - Visual Studio Marketplace

Qiitaで記事書いている人もいました

---以下、日記です…---

.envファイルを作って、キーを…

Twitter APIを久々に使おうと思った時

twitter - npmここから

varTwitter=require('twitter');varclient=newTwitter({consumer_key:'',consumer_secret:'',access_token_key:'',access_token_secret:''});varparams={screen_name:'nodejs'};client.get('statuses/user_timeline',params,function(error,tweets,response){if(!error){console.log(tweets);}});

ここをコピーして、

.envファイルを作って

Image from Gyazo

以下の部分をコピー

consumer_key:'',consumer_secret:'',access_token_key:'',access_token_secret:''

olt+Shiftで矩形選択して前の方にtwitter_ってつけてみた

Image from Gyazo

後ろの方を選択して、=に変えてみまして

Image from Gyazo

ここから拡張機能使います!

上記の「change-case」拡張機能を入れてから、 cmd+Shift+pから、「Change Case upper」を選択

Image from Gyazo

完成!

Image from Gyazo

Twitterのデベロッパー管理画面からのコピーは、まぁがんばろう

ソースコード用に

1.以下の部分をコピー

Image from Gyazo

2.また矩形選択して、前の方に挿入

Image from Gyazo

3.(最後の行にカンマをつけて)「'',」部分を選択肢、コマンド+D

Image from Gyazo

4.process.env.を記入

Image from Gyazo

5.2〜4行目の行頭を矩形選択し、左キーを押して1〜3行の末にカンマを入れる

Image from Gyazo

6.コードへコピペ!!

Image from Gyazo

おつかれさまでした!

playwright で E2E テスト!

$
0
0

playwright.png

はじめに

みなさん、Playwright をご存知ですか?

これまで、Node.js での E2E テストといえば、puppeteer、TestCafe を使っていたという方も少なくないのではないでしょうか?

Playwright は、そのうち、puppeteer と同じような記述も多く、非常に分かりやすいかと思います。

また、Microsoft によって開発、運用されているため、今後サポートされなくなるというリスクも
ある程度回避できるかと思います。

2020/12/26 時点では、バージョン 1.7.0 なので、その時点での情報になります。

サポート環境

2020/12/26 時点でサポートしているのは以下になります。

  • Node.js 10.17 以上
  • Windows: Windows 及び WSL で動きます
  • macOS: 10.14 以上
  • Linux: ディストリビューションによる(Firefox は、Ubuntu 18.04 以上)

古い Microsoft Edge や IE 11 はサポートされていません。

また、Python や C# などでも使えますが、Java や Ruby はサポートしていません。

インストール

$ yarn add -D playwright

簡単な例

Yahoo! JAPAN トップページにアクセスして、スクリーンショットを撮るテストをしてみます。

test.js
const{chromium,devices}=require('playwright');(async()=>{constbrowser=awaitchromium.launch();constpage=awaitbrowser.newPage({...devices['iPhone 11 Pro']});awaitpage.goto('https://m.yahoo.co.jp');awaitpage.screenshot({path:'./screenshot.png',fullPage:true});awaitbrowser.close();})()

デバックモードで実行するとより詳細にテスト内容が確認できます。

$ DEBUG=pw:api node test.js

各 API について

全て書くと多すぎるので、使えそうなものだけまとめてみます。

playwright

playwright.chromium

Chromium ブラウザを使用する際に使います。

playwright.firefox

FireFox ブラウザを使用する際に使います。

playwright.webkit

Webkit ブラウザを使用する際に使います。

playwright.devices

テストを行うデバイスを指定します。
指定できるのは、

に記載のあるものになります。

また、同じ形式で連想配列にして指定することで独自のものを指定することも可能です。

Browser

browser.close

browserType.launch や browserType.connect で生成されたブラウザを閉じます。

browserType.launch で生成されたブラウザに対しては、全てのページも閉じます。

browserType.connect で生成されたブラウザに対しては、全ての context をリセットし、サーバとの接続も解除します。

browser.newContext

引数に連想配列を指定して、context を生成します。

いくつか抜粋します。

ignoreHTTPSErrors: HTTPS のエラーを無視します。デフォルトは false です。

userAgent: User-Agent を指定します。

isMobile: モバイルデバイスかどうかを指定します。Firefox は、サポートされていません。

hasTouch: viewport がタッチイベントをサポートしているかどうかです。デフォルトは false です。

geolocation<{latitude: number, longitude: number, accuract: number}>: 位置情報について指定します。

extraHTTPHeaders<{[key: string]: string}>: HTTP ヘッダを指定します。

browser.newPage

引数に連想配列を指定して、context の中で page を生成します。

これは、SPA や短いコードに対して便利な API になります。
プロダクションコードでは、browser.newContext を行ってから browser.newPage を行ってください。

引数は、browser.newContext とほとんど同じになります。

BrowserContext

browserContext.cookies

設定されている cookie のリストを取得します。

引数に url を指定することで特定の cookie に絞り込むことができます。

browserContext.addCookies

cookie を設定します。

browserContext.clearCookies

cookie をリセットします。

browserContext.storageState

cookie や LocalStorage の値を取得します。

browserContext.grantPermissions

さまざまなパーミッションを与えます。

指定できるのは、以下のものになります。

  • 'geolocation'
  • 'midi'
  • 'midi-sysex' (system-exclusive midi)
  • 'notifications'
  • 'push'
  • 'camera'
  • 'microphone'
  • 'background-sync'
  • 'ambient-light-sensor'
  • 'accelerometer'
  • 'gyroscope'
  • 'magnetometer'
  • 'accessibility-events'
  • 'clipboard-read'
  • 'clipboard-write'
  • 'payment-handler'

browserContext.clearPermissions

パーミッションをリセットします。

browserContext.exposeBinding

window オブジェクトに関数を追加します。

これは、全ての frame 、page に追加されます。

例:

const{webkit}=require("playwright");// Or 'chromium' or 'firefox'.(async()=>{constbrowser=awaitwebkit.launch({headless:false});constcontext=awaitbrowser.newContext();awaitcontext.exposeBinding("pageURL",({page})=>page.url());constpage=awaitcontext.newPage();awaitpage.setContent(`
    <script>
      async function onClick() {
        document.querySelector('div').textContent = await window.pageURL();
      }
    </script>
    <button onclick="onClick()">Click me</button>
    <div></div>
  `);awaitpage.click("button");})();

browserContext.exposeFunction

window オブジェクトに関数を追加します。

これは、全ての frame 、page に追加されます。

例:

const{webkit}=require("playwright");// Or 'chromium' or 'firefox'.constcrypto=require("crypto");(async()=>{constbrowser=awaitwebkit.launch({headless:false});constcontext=awaitbrowser.newContext();awaitcontext.exposeFunction("md5",(text)=>crypto.createHash("md5").update(text).digest("hex"));constpage=awaitcontext.newPage();awaitpage.setContent(`
    <script>
      async function onClick() {
        document.querySelector('div').textContent = await window.md5('PLAYWRIGHT');
      }
    </script>
    <button onclick="onClick()">Click me</button>
    <div></div>
  `);awaitpage.click("button");})();

browserContext.newPage

page を生成します。

browserContext.close

context を閉じます。

browserContext.route

ネットワークリクエストをキャッチして、特定の処理を行うようにハンドリングします。

特定の url で一度設定したら、マッチする全てのリクエストがハンドリングされます。

例えば、画像を取得するリクエストを無視する場合、以下のようになります。

constcontext=awaitbrowser.newContext();awaitcontext.route("**/*.{png,jpg,jpeg}",(route)=>route.abort());constpage=awaitcontext.newPage();awaitpage.goto("https://example.com");awaitbrowser.close();

browserContext.unroute

設定した route を解除します。

browserContext.setDefaultNavigationTimeout

navigation タイムアウトを設定します。

影響を与えるのは、以下の API になります。

  • page.goBack
  • page.goForward
  • page.goto
  • page.reload
  • page.setContent
  • page.waitForNavigation

browserContext.setDefaultTimeout

全ての API のタイムアウトを設定します。

ただし、page.setDefaultNavigationTimeout, page.setDefaultTimeout(timeout), browserContext.setDefaultNavigationTimeout(timeout) の方が優先されます。

browserContext.setGeolocation

位置情報を設定します。

この位置情報を取得するためには、browserContext.grantPermissions で権限を与える必要があります。

browserContext.waitForEvent

特定の event が発火されるまで待ちます。

Page

page.on('dialog')

alert, prompt, confirm, beforeunload が呼ばれた時に
呼ばれるイベントリスナーです。

page.on('request')

リクエストが発行された時に呼ばれるイベントリスナーです。

コールバックの引数となる request は、読み込み専用です。

page.on('requestfailed')

タイムアウトなどリクエストが失敗した時に呼ばれるイベントリスナーです。

page.on('requestfinished')

レスポンスボディの取得が成功したときに呼ばれるイベントリスナーです。

page.on('response')

リクエストヘッダ、ステータスを受け取った時に呼ばれるイベントリスナーです。

page.$

セレクタを1つだけ指定します。

複数マッチした時は、最初の1つ目のみのセレクタとなります。

1つもマッチしなかった場合は、null を返します。

page.$$

セレクタを複数指定します。

1つもマッチしなかった場合は、[] を返します。

page.$eval

セレクタを1つだけ指定して、処理を行います。
セレクタがマッチしなかった場合は、エラーを投げます。

constsearchValue=awaitpage.$eval("#search",(el)=>el.value);constpreloadHref=awaitpage.$eval("link[rel=preload]",(el)=>el.href);consthtml=awaitpage.$eval(".main-container",(e,suffix)=>e.outerHTML+suffix,"hello");

page.$$eval

セレクタを複数指定して、処理を行います。

constdivsCounts=awaitpage.$$eval("div",(divs,min)=>divs.length>=min,10);

page.check

セレクタを指定して、チェックを入れます。

以下の順番で処理が行われます。

  1. マッチするセレクタを探します。もしなければ、マッチする DOM が生成されるまで待ちます。
  2. input の checkbox か radio かどうかを確認します。もし違った場合は、reject します。既にチェック済みの場合は、すぐに return します。
  3. force オプションが指定されていない場合は、チェックされるまで待ちます。
  4. 必要があれば、スクロールを行います。
  5. page.mouse を使って、セレクタの真ん中をクリックします。
  6. noWaitAfter オプションが指定されていない場合は、ナビゲーションが成功するかどうかを待ちます。
  7. セレクタがチェックされたかを確認します。されていなければ、reject します。

page.uncheck

セレクタを指定して、チェックを外します。

処理は page.check と同じ流れで行います。

page.click

セレクタを指定して、クリックします。

以下の順番で処理が行われます。

  1. マッチするセレクタを探します。もしなければ、マッチする DOM が生成されるまで待ちます。
  2. force オプションが指定されていない場合は、クリックできるセレクタ可動かをチェックします。
  3. 必要があれば、スクロールを行います。
  4. page.mouse を使って、セレクタの真ん中か特定の位置をクリックします。
  5. noWaitAfter オプションが指定されていない場合は、ナビゲーションが成功するかどうかを待ちます。

page.close

page を閉じます。

runBeforeUnload オプションが false の場合は、結果は page を閉じてから返します。

runBeforeUnload オプションが true の場合は、page が閉じるのを待ちません。

page.dblclick

セレクタを指定して、ダブルクリックします。

page.dispatchEvent

セレクタを指定して、イベントを発火させます。

複数セレクタがマッチした場合は、最初の1つ目になります。

awaitpage.dispatchEvent("button#submit","click");

page.fill

セレクタを指定して、値を入力します。

複数セレクタがマッチした場合は、最初の1つ目になります。

input や textarea など、入力できないものにマッチした場合は、エラーを投げます。

page.focus

セレクタを指定して、フォーカスします。

複数セレクタがマッチした場合は、最初の1つ目になります。

マッチするセレクタが見つからない場合は、見つかるまで待ちます。

page.getAttribute

セレクタを指定して、属性値を取得します。

複数セレクタがマッチした場合は、最初の1つ目になります。

page.goBack

一つ前の画面に戻ります。

page.goForward

一つ先の画面に進みます。

page.goto

指定された url に遷移します。

以下の場合は、エラーを投げます。

  • SSL エラー
  • 不正な URL
  • タイムアウト
  • サーバからレスポンスが返ってこない
  • メインリソースのロードに失敗

page.reload

ページを再読み込みします。

page.hover

セレクタを指定して、ホバーします。

page.innerHTML

セレクタを指定して、HTML 要素を取得します。

複数セレクタがマッチした場合は、最初の1つ目になります。

page.innerText

セレクタを指定して、テキスト要素を取得します。

複数セレクタがマッチした場合は、最初の1つ目になります。

page.press

セレクタを指定して、キーを押します。

複数セレクタがマッチした場合は、最初の1つ目になります。

キーは、

F1 - F12, Digit0- Digit9, KeyA- KeyZ, Backquote, Minus, Equal, Backslash, Backspace, Tab, Delete, Escape, ArrowDown, End, Enter, Home, Insert, PageDown, PageUp, ArrowRight, ArrowUp

などが選択できます。

また、複数同時に押すことも可能です。

以下に例を示します。

constpage=awaitbrowser.newPage();awaitpage.goto("https://keycode.info");awaitpage.press("body","A");awaitpage.screenshot({path:"A.png"});awaitpage.press("body","ArrowLeft");awaitpage.screenshot({path:"ArrowLeft.png"});awaitpage.press("body","Shift+O");awaitpage.screenshot({path:"O.png"});awaitbrowser.close();

page.route

マッチした url に対して、特定の処理を行います。

一度設定されると全てのリクエストに対して有効です。

constpage=awaitbrowser.newPage();awaitpage.route("**/*.{png,jpg,jpeg}",(route)=>route.abort());awaitpage.goto("https://example.com");awaitbrowser.close();

page.unroute

page.route を解除します。

page.screenshot

スクリーンショットを保存します。

保存できるのは、png または jpeg のみになります。

page.selectOption

セレクタを指定して、select の要素を選択します。

// single selection matching the valuepage.selectOption("select#colors","blue");// single selection matching both the value and the labelpage.selectOption("select#colors",{label:"Blue"});// multiple selectionpage.selectOption("select#colors",["red","green","blue"]);

page.setContent

HTML をページに設定します。

consthtml=`<!DOCTYPE html>
<html>
<head><title>test</title></head>
<body>
<h1>test</h1>
</body>
</html>`;awaitpage.setContent(html);

page.setDefaultNavigationTimeout

navigation タイムアウトを設定します。

影響を与えるのは、以下の API になります。

  • page.goBack
  • page.goForward
  • page.goto
  • page.reload
  • page.setContent
  • page.waitForNavigation

page.setDefaultTimeout

全ての API のタイムアウトを設定します。

page.textContent

セレクタを指定して、中身を取得します。

page.title

ページのタイトルを取得します。

page.type

セレクタを指定して、文字をタイプします。

awaitpage.type("#mytextarea","Hello");awaitpage.type("#mytextarea","World",{delay:100});

page.url

ページの URL を取得します。

page.waitForEvent

指定したイベントが発火されるまで待ちます。

page.waitForFunction

引数で記述した処理が true になるまで待ちます。

const{webkit}=require("playwright");(async()=>{constbrowser=awaitwebkit.launch();constpage=awaitbrowser.newPage();constwatchDog=page.waitForFunction("window.innerWidth < 100");awaitpage.setViewportSize({width:50,height:50});awaitwatchDog;awaitbrowser.close();})();

page.waitForLoadState

"load" または "domcontentloaded" または "networkidle" の状態になるまで待ちます。

const[popup]=awaitPromise.all([page.waitForEvent("popup"),page.click("button"),]);awaitpopup.waitForLoadState("domcontentloaded");console.log(awaitpopup.title());

page.waitForNavigation

ページ遷移が終わるまで待ちます。

const[response]=awaitPromise.all([page.waitForNavigation(),page.click("a.delayed-navigation"),]);

page.waitForRequest

指定されたリクエストが来るまで待ちます。

戻り値には、マッチしたリクエストが返ってきます。

constfirstRequest=awaitpage.waitForRequest("http://example.com/resource");constfinalRequest=awaitpage.waitForRequest((request)=>request.url()==="http://example.com"&&request.method()==="GET");returnfirstRequest.url();

page.waitForResponse

指定されたレスポンスが来るまで待ちます。

戻り値には、マッチしたリクエストのレスポンスが返ってきます。

constfirstResponse=awaitpage.waitForResponse("https://example.com/resource");constfinalResponse=awaitpage.waitForResponse((response)=>response.url()==="https://example.com"&&response.status()===200);returnfinalResponse.ok();

page.waitForSelector

セレクタを指定して、そのセレクタが特定のステータスになるまで待ちます。

ステータスは、attacheddetachedvisiblehiddenから指定できます。

const{chromium}=require("playwright");(async()=>{constbrowser=awaitchromium.launch();constpage=awaitbrowser.newPage();letcurrentURL;page.waitForSelector("img").then(()=>console.log("First URL with image: "+currentURL));for(currentURLof["https://example.com","https://google.com","https://bbc.com",]){awaitpage.goto(currentURL);}awaitbrowser.close();})();

page.waitForTimeout

指定したミリ秒数待ちます。

Dialog

dialog.accept

prompt ダイアログで OK されるまで待ちます。

dialog.defaultValue

prompt ダイアログの場合は、その文字列を、それ以外のダイアログの場合は、空文字列を返します。

dialog.dismiss

ダイアログが閉じられるまで待ちます。

const{chromium}=require("playwright");(async()=>{constbrowser=awaitchromium.launch();constpage=awaitbrowser.newPage();page.on("dialog",async(dialog)=>{console.log(dialog.message());awaitdialog.dismiss();awaitbrowser.close();});page.evaluate(()=>alert("1"));})();

dialog.message

表示されているダイアログのメッセージを取得します。

dialog.type

どんなダイアログを表示しているか取得します。

取得できるのは、alertbeforeunloadconfirmpromptです。

Keyboard

keyboard.down

指定したキーを keyDown イベントと一緒に入力します。

keyboard.up

指定したキーを keyUp イベントと一緒に入力します。

keyboard.press

指定したキーを keyPress イベントと一緒に入力します。

keyboard.insertText

指定した文字を入力します。

keyDownkeyUpkeyPressイベントは発行しません。

keyboard.type

指定した文字を入力します。

keyDownkeyUpkeyPressイベントは発行します。

EvaluationArgument

Playwright は、jest による評価も可能ですが、Playwright にも評価するメソッドが存在します。

// A primitive value.awaitpage.evaluate((num)=>num,42);// An array.awaitpage.evaluate((array)=>array.length,[1,2,3]);// An object.awaitpage.evaluate((object)=>object.foo,{foo:"bar"});// A single handle.constbutton=awaitpage.$("button");awaitpage.evaluate((button)=>button.textContent,button);// Alternative notation using elementHandle.evaluate.awaitbutton.evaluate((button,from)=>button.textContent.substring(from),5);// Object with multiple handles.constbutton1=awaitpage.$(".button1");constbutton2=awaitpage.$(".button2");awaitpage.evaluate((o)=>o.button1.textContent+o.button2.textContent,{button1,button2,});// Obejct destructuring works. Note that property names must match// between the destructured object and the argument.// Also note the required parenthesis.awaitpage.evaluate(({button1,button2})=>button1.textContent+button2.textContent,{button1,button2});// Array works as well. Arbitrary names can be used for destructuring.// Note the required parenthesis.awaitpage.evaluate(([b1,b2])=>b1.textContent+b2.textContent,[button1,button2,]);// Any non-cyclic mix of serializables and handles works.awaitpage.evaluate((x)=>x.button1.textContent+x.list[0].textContent+String(x.foo),{button1,list:[button2],foo:null});

Working with Chrome Extensions

Playwright は、ヘッドレスモードでない状態であれば、Chrome Extensions を有効にした状態でのテストも可能です。

const{chromium}=require('playwright');(async()=>{constpathToExtension=require('path').join(__dirname,'my-extension');constuserDataDir='/tmp/test-user-data-dir';constbrowserContext=awaitchromium.launchPersistentContext(userDataDir,{headless:false,args:[`--disable-extensions-except=${pathToExtension}`,`--load-extension=${pathToExtension}`]});constbackgroundPage=browserContext.backgroundPages()[0];// Test the background page as you would any other page.awaitbrowserContext.close();})();

おわりに

いかがだったでしょうか?

自分が作成したプロダクトに対して、E2E テストを書くことも出来ますし、
さまざまなブラウザを使った自動化にも役立つかと思います。

日々の面倒な作業を Playwright を使って自動化するのも良いかもしれません。

puppeteer を使っていた方であれば、より多くのブラウザをサポートしている Playwright には
メリットも感じられるかと思うので、ぜひ移行してみてください。

参考

【Heroku】デプロイしたときにおこるApplication error(code=H10)の対処法

$
0
0

プログラミング勉強日記

2021年3月20日
ローカルでは動くのに、HerokuでデプロイするとApplication errorになってしまったので、その対処法を紹介する。

image.png

エラー内容

 まず書かれているように、heroku logs --tailでログを確認した。

2021-03-19T06:17:26.458574+00:00 heroku[router]: at=error code=H10 desc="App crashed" method=GET path="/favicon.ico" host=videochat-sample.herokuapp.com request_id=48b111ba-aaf9-49c2-b454-ffb81ccb1fcb fwd="14.14.208.82" dyno= connect= service= status=503 bytes= protocol=https

 すると、code=H10でApp crashedと書いてある。

やってみたこと

 ネットで調べてみると、Herokuを再起動すると書いてあった。

$ heroku restart -app アプリ名

 次に、起動してるプロセスを確認してそのプロセスを再起動することをやってみた。

$ heroku ps
$ heroku restart web.1

 どれをやっても動かなかった。

 Herokuのコンソールを見る方法があって、やってみたがそれでも見当たらなかった。

Herokuのコンソールを見る方法
$ heroku run console

解決方法

 自分のコードの場合、package.jsonに誤りがあった。scriptの設定がデフォルトのままになっていた。これを直したら正しく表示された。

package.json
{"name":"videochat-sample","version":"1.0.0","description":"","main":"index.js","scripts":{"start":"node app.js","test":"echo \"Error: no test specified\"&& exit 1"},"author":"","license":"ISC","dependencies":{"express":"^4.17.1"}}

参考文献

Herokuでアプリが開けない!H10 App Crashed対処法メモ
Heroku展開エラーH10(アプリがクラッシュしました)

Node.jsのnpmモジュールを使ってjsonファイルをminify・整形する手順

$
0
0

はじめに

Windows環境で、Node.jsのnpmモジュールを使って以下を行う手順をまとめます。

  • JSONファイルの軽量化(minify):改行やスペースの削除
  • JSONファイルの整形:改行やインデントにより見やすくする

使用モジュール

準備:モジュールのインストール

  1. Node.jsを取得しインストールする。→ https://nodejs.org/ja/

  2. Node.js command promptを起動する。

  3. 以下のコマンドを実行する。(2行目のプロキシ設定は必要な場合のみ実行)

$ npm -g config set registry http://registry.npmjs.org/
$ npm -g config set proxy http://xxx.xxx.xxx:8080
$ npm install -g json-minify
$ npm install -g format-jsonfile

使用方法

Node.js command promptから、以下のコマンドを実行する。

JSONファイルの軽量化

$ type test1.json
  [
    {
      "name": "foo",
      "age": 10
    },
    {
      "name": "bar",
      "age": 20
    },
    {
      "name": "hoge",
      "age": 30
    }
  ]
$ json-minify test1.json
  [{"name":"foo","age":10},{"name":"bar","age":20},{"name":"hoge","age":30}]

JSONファイルの整形

$ type test2.json
  [{"name":"foo","age":10},{"name":"bar","age":20},{"name":"hoge","age":30}]
$ format-jsonfile test2.json
  [
    {
      "name": "foo",
      "age": 10
    },
    {
      "name": "bar",
      "age": 20
    },
    {
      "name": "hoge",
      "age": 30
    }
  ]

参考記事

Firebaseでサブコレクションまで再帰的に削除する上での作業備忘録

$
0
0

作業備忘録なので、自分がわかるようにしか書きません。申し訳ありませんが、参考にならないと思います。

FirebaseのFirestoreにおいて、ドキュメントを削除しても、ドキュメント配下のサブコレクションは削除されない問題がある。
公式では、FirebaseのFunctionsサービスを用いて、サーバサイドに削除関数を配置してクライアント側から呼び出すように設定することを推奨している。

Functionsのサンプルコードは以下になる。
https://github.com/firebase/snippets-node/blob/master/firestore/solution-deletes/functions/index.js

Functionsを使えるようになるまでにもろもろの設定があるが(設定手順は紹介しない)、デフォルトのままだと上記のコードをデプロイしてもエラーが起きる。

参考までにデプロイ時のコードを以下に載せる。

sudo firebase deploy --only functions

エラー内容は以下のようなものになる。

function terminated. Recommended action: inspect logs for termination reason. Additional troubleshooting documentation can be found at https://cloud.google.com/functions/docs/troubleshooting#logging Function cannot be initialized

原因はパッケージ不足だった。
プロジェクトフォルダ/functions/上で以下のコマンドを実行することで、firebase-toolsが使用できるようになるのでデプロイが成功するようになる。
(プロジェクトフォルダ/node_modulesとプロジェクトフォルダ/functions/node_modulesが共有でないことに気づくまでに時間がかかったため、備忘録として追加しておく)

sudo npm install--save firebase-tools

デプロイが成功したため、今度はフロント側から呼び出すコードが必要になる。
以下の公式が推奨する手順の「クライアントからの呼び出し」という項目を進めていく。
https://firebase.google.com/docs/firestore/solutions/delete-collections?hl=ja

完了し次第、備忘録に追加する予定...

ApacheでJavaアプリとNodeアプリを動かす

$
0
0

はじめに

もともとApatch + TomcatでJavaアプリケーションを稼働しているサーバーがあり、
そこにReact + Node + Typescriptで作ったアプリケーションを稼働させたいという要望があり(私の要望)それを実現するためにやったことを書いていこうと思います。

参考にしたサイトはこちら

前提条件

  • Javaのアプリケーションは稼働済み
  • Javaアプリケーションはxxx.xxx.xxxでアクセス可(ルート)
  • Nodeアプリケーションはxxx.xxx.xxx/node/でアクセス可にする
  • Nodeアプリケーションの稼働ポートは3000
  • localhost:3000/node/でNodeアプリケーションが稼働している

設定値

特定のURLにアクセスした場合に違うパスにアクセスさせる。(ブラウザで見えるアクセスURLは変わりません。)

xxx.xxx.xxx/node/

localhost:3000/node/

conf/httpd.conf
#コメントを外して有効にする
LoadModule proxy_http_module modules/mod_proxy_http.so

LoadModule proxy_http_module modules/mod_proxy.soもコメントで無効の場合は、
コメントを外して有効にする。

conf/extra/httpd-proxy-ajp.conf
# 以下を追記
<Location /node/>
  ProxyPass http://localhost:3000/
  ProxyPassReverse http://localhost:3000/
  Require all granted
</Location>

アクセスのパスを変更したい場合はLocation /node//node/を変更することで実現できます。

元々はhttpd-proxy-ajp.confのファイルにajpに関する設定があり今回は同じファイルに書いていますが、おそらく上記参考サイトのようにhttpd-proxy.confのようなファイルを作ってそちらに書いたほうがわかりやすいと思います。

Node.jsでgRPCを動かそう

$
0
0

※こちらの記事は3/21に開催するxhack勉強会「Node.jsでgRPCを動かそう」の資料となります

本日のお品書き

始めに

ぜひ#xhack勉強会 で感想をツイートしてください!
また、可能な方はカメラオンにして欲しいです!顔が見えない中ひとりで話すのは寂しい、、

自己紹介

とむといいます。
新卒未経験からエンジニアになってまもなく2年。
今年の2月に転職をし、大阪から東京に引っ越してきました。
転職前はJavaを使って主にサーバ側の開発をしていましたが、現在はスマホアプリ開発をメインに行っている株式会社マンハッタンコードで働いています。
Twitterはこちらをクリック←ぜひフォローしてください!

以前からX-HACKさんの勉強会にはものすごくお世話になっていたのですが、今回初めて勉強会を主催させていただきます。
お手柔らかにお願いします、、

想定参加者

  • gRPCって聞いてピンと来ない人
  • JavaScriptが読める人
  • サーバ、クライアント、API、HTTPといった用語をある程度理解している人
  • Macを持っている人
  • ターミナルで基本的なコマンドが扱える人(npm・nodeコマンドがインストールされている人)

今日のゴール

注意:今回の勉強会では書き方の細かいルールなどは説明しません!

Node.jsでgRPCを動かそう

gRPCって何?

  • Googleが公開したRPCフレームワークの一つ

そもそもRPCって???

RPCはRemote Procedure Callの略。
たけがみまさきさん著のスターティングgRPCでは

Remote Procedure Callを訳すと遠隔手続き呼び出しという意味になります。すなわち、あるサービスからべつのサービスのアプリケーションの処理(サブルーチン/クラス/関数など)を呼び出す技術ということです。

と説明されています。
遠くにある関数とかを呼び出せちゃう便利なやつがRPCで、gRPCはその中の1つということをふんわりと頭に入れておいてもらえたらOKです!

gRPCのここがすごい

  • HTTP/2によって高速な通信ができる
  • ProtocolBuffers
    • データをシリアライズ化する仕組み
    • 独自の書き方(IDL=インターフェース定義言語)で呼び出しの定義ができる!!→異なる言語を使っているサービスの処理も呼び出せる!!! image.png

ここからは実際に手を動かしていきましょう!

ハンズオン

基本的な流れはこちら。
1. 必要なツールやライブラリをインストールする
2. Protocol Buffersを使って定義ファイルを書く
3. (定義ファイルをコンパイルしてコードを生成する)
4. 定義に沿ってサーバとクライアントを実装する

公式チュートリアルを参考に、よりシンプルなものを作ってみましょう

作るもの
指定した人に挨拶をするアプリみたいなやつ

  • クライアント側:誰に挨拶するかを決める
  • サーバ側:挨拶するフレーズを生成

1. 必要なツールやライブラリをインストールする

npm init

npm install grpc @grpc/proto-loader
or
npm install @grpc/grpc-js @grpc/proto-loader

grpcパッケージじゃなく下のgrpc-jsの方がいいかも?
参考

2. Protocol Buffersを使って定義ファイルを書く

helloworld.proto
// Protocol Buffersのバージョンを指定
syntax = "proto3";

// パッケージを指定
package helloworld;

// サービスを定義
service Greeter {
  // 引数の型:HelloRequestメッセージ
  // 戻り値の型:HelloReplyメッセージ
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// メッセージの型を定義
message HelloRequest {
  // 各フィールドに識別子としてタグを付ける
  // 値をフィールドに代入しているわけではないことに注意
  string name = 1;
}

message HelloReply {
  string message = 1;
}

3. (定義ファイルをコンパイルしてコードを生成する)

node.jsはコンパイルをせず2.で作成した定義ファイルを読み込めばヨシ!!

4. 定義に沿ってサーバとクライアントを実装する

サーバ側

クライアントからアクセスがあったら挨拶文を生成する(sayHelloする)サーバを動かしたい

server.js
constprotoLoader=require('@grpc/proto-loader');// インストールしたパッケージに合わせる// const grpc = require('grpc');constgrpc=require('@grpc/grpc-js');constPROTO_PATH=__dirname+'/helloworld.proto'// 定義ファイル(.protoファイル)の読み込みconstpackageDefinition=protoLoader.loadSync(PROTO_PATH,{keepCase:true,longs:String,enums:String,defaults:true,oneofs:true});consthello_proto=grpc.loadPackageDefinition(packageDefinition).helloworld;/**
 * sayHelloメソッド
 */functionsayHello(call,callback){callback(null,{message:'Hello '+call.request.name});}/**
 * mainメソッド
 */functionmain(){// サーバのインスタンスを生成constserver=newgrpc.Server();// サーバがGreeterサービスのリクエストを受け取るようにするserver.addService(hello_proto.Greeter.service,{sayHello:sayHello});// クライアントのリクエストをリッスンする(外部からのアクセスに備えて待機する)ためのアドレスとポートを指定server.bindAsync('127.0.0.1:50051',grpc.ServerCredentials.createInsecure(),()=>{// サーバを起動するserver.start();});}main();

サーバを起動!

node server.js

クライアント側
クライアントがやりたいのは

  • 誰に挨拶をするか決める
  • サーバ側(Remote)のsayHello(Procedure)を呼び出し(Call)
client.js
constPROTO_PATH=__dirname+'/helloworld.proto';// インストールしたパッケージに合わせる// const grpc = require('grpc');constgrpc=require('@grpc/grpc-js');// 定義ファイル(.protoファイル)の読み込みconstprotoLoader=require('@grpc/proto-loader');constpackageDefinition=protoLoader.loadSync(PROTO_PATH,{keepCase:true,longs:String,enums:String,defaults:true,oneofs:true});consthello_proto=grpc.loadPackageDefinition(packageDefinition).helloworld;/**
 * mainメソッド
 */functionmain(){// サーバのアドレスとポートを指定してGreeterサービスのスタブコンストラクタを呼び出すconstclient=newhello_proto.Greeter('127.0.0.1:50051',grpc.credentials.createInsecure());// スタブでsayHelloメソッドの呼び出し// リクエスト(HelloRequestメッセージ型のオブジェクト)とコールバック関数をサーバ側に渡すclient.sayHello({name:'World'},function(err,response){console.log('Greeting:',response.message);});}main();

クライアントを起動!

node server.js

Greeting:Hello Worldと出力されれば成功です!!

お疲れ様でした!!

最後に

初めての勉強会開催だったので皆さんの感想が気になります!!
#xhack勉強会でツイートしてくれたら嬉しいです!

近日開催されるxhack勉強会

株式会社マンハッタンコード

現在私が所属している株式会社マンハッタンコードでは、スマホアプリ開発を主軸にさまざまな挑戦をしています。
毎週土曜日にはイベントを開催しているので、気軽に遊びに来てください!


最後に、今回の勉強会を開く後押しをしてくださり、当日も参加してサポートしてくださったX-HACKの松田さんとよももさん、そして日曜の夜にわざわざ時間を作って参加してくださった皆さん、ありがとうございました!

参考


Node.jsのMongodbをPromiseで行ったチートシート(?)

$
0
0

初めに

Node.jsでMongodbを操作したいという際にもうちょっと簡単で楽にならないかなと考えて作ったのが今回のプログラムです。楽is神。

前提

・Node.jsのインストールを済ませていること
・Mongodbのインストールを済ませていること
・npmのインストールを済ませていること
・mongodbのNode.jsのAPIをインストールしていること

環境

・さくらVPSのCentos7
・node.js v15.11.0
・npm 7.6.2
・mongodb v4.4.4

Mongodb

わかりやすいようにMongodbを操作する用のディレクトリをとりあえず作っておきましょう。

$mkdir database
$cd database

接続部分

まずはMongodbとの接続についてのファイルを作成します。

$vim mongo_connect.js
node.js
//mongo_connect.jsconstMongoClient=require('mongodb').MongoClientfunctionopen(){leturl='mongodb://ユーザー名:パスワード@localhost:27017';constconnectOptions={useNewUrlParser:true,useUnifiedTopology:true,}returnnewPromise((resolve,reject)=>{MongoClient.connect(url,connectOptions,(err,db)=>{if(err)reject(err);elseresolve(db);});});}functionclose(db){if(db)db.close();}letdb={open:open,close:close}module.exports=db;

urlについてですがユーザーを作成していた場合は以上のとおりになります。ユーザー名とパスワードを設定したとおりに入れてくださいね。
認証を設定したいという方は

こちらの記事の最後の方に書いてあるので、是非参考にしてくださいな。
またMongodbをwebサイトのデータベースとしてでもなんでも、本格的に運用したいという方は27017からポートの変更をしておいたほうがいいです。今回はテストも兼ねていたのでデフォルトのポートのままです。

クエリ部分

Mongodbにおいてもクエリと呼んでいいのかどうかはわかりませんがとにかくクエリです。

$vim insertOne.js
node.js
//insertOne.jsconstzenodb=require('./mongo_connect.js');functioninsertOne(object,col){letdatabase=null;zenodb.open().then((db)=>{database=db;constdbName=db.db("自分の使うデータベース名を入力してね!");returndbName.collection(col);}).then((collection)=>{returncollection.insertOne(object);}).catch((err)=>{console.error(err)}).finally(()=>{zenodb.close(database);})}module.exports=insertOne;

insertOne({データ名,データ},コレクション名)とすることで実行できるメソッドを用意しました。requireしている接続ファイルがzenodbですが、参考にさせていただいたQ&Aの回答者の人がそう書いていたのでそう書かせてもらいました。意味は・・・調べてみたんですがよくわからなかったっす。
念の為、参考にさせていただいたQ&Aのサイトを載せておきます。

https://stackoverflow.com/questions/37911838/how-to-use-mongodb-with-promises-in-node-js

集合部分

今回はinsertOneだけでしたが、他にも作れば当然便利になるでしょう。しかしそうしたらrequireするのがめんどくせぇ!とかなりそうなので、集合部分を予め作っておきましょう。

$vim methods.js
node.js
constinsertOne=require("./insertOne.js");letmethods={insertOne:insertOne,}module.exports=methods;

let methodsの場所にクエリをたくさん追加できるようになれば便利ですね。
最後に一応今回作ったプログラムを試しておきましょう。
使用するのは先程作ったmethodsファイル。

node.js
constinsertOne=require("./insertOne.js");letmethods={insertOne:insertOne,}methods.insertOne({name:"tekitou"},"users");module.exports=methods;
node methods.js

これでmongodbシェルにログインしてみて、対象のデータベースのコレクションの中にデータが入っていればOKです。
以上!
なにか質問があれば書いてください!わからないかもしれませんけどねw

Windowsユーザー必見!scoopを使ってコマンドからGit、Node、npm、PHPをダウンロードしてHomebrewを実現しよう!!

$
0
0

皆さんこんにちは!

今日はWinodwsでコマンドから色々ものをダウンロードして見ようかなと思います。

良く解説動画を見ると、Macで説明されている方が多くその度にHomebrewと言う言葉を耳にします。

残念ながらWindowsではこのHomebrewと言うものは存在しません(多分)。

ですが、Homebrewと同じような機能をもつものがWindowsにも搭載されています!

その名はscoop!!

これを使えば、コマンドからNodeやらGitやらをインストールすることができます。

他にもscoopを使えばバージョンの指定を行うことも可能なので、わざわざサイトからダウンロードするなんて作業にはもうさよならです。

scoopをインストールするにはPowerShellでの作業が必須となりますのでご注意を。

それでは早速scoopを使って色々やってみましょう!

scoopのインストール

もう一度言います。

PowerShellで行ってくださいね。

Set-ExecutionPolicy RemoteSigned -scope CurrentUser
// 実行ポリシーの変更について聞かれるので「Y」を選択
iex (new-object net.webclient).downloadstring('https://get.scoop.sh')
scoop bucket add extras
scoop bucket add versions

この2つは良く使うのでインストールしてください。

Gitの導入

scoop install git

これでGitをインストールすることができました!

git --version

バージョンが返ってくればOK!

環境変数のPATHも自動で通してくれるのでめちゃ便利!

ついでにopensshも取得

scoop install openssh

Nodeのインストール

お次はNodeをインストールし見ましょう!

scoop install nodejs

バージョン指定

バージョン指定したい場合は以下のように実行

scoop install nodejs8

ただし、scoop bucket add versionsを行っていない場合は出来ないので注意。

node -v

npm -v

バージョンが返ってくればOK!

PHPの導入

ここからはおまけみたいな感じです。

scoop install php-nts php-nts-xdebug vcredist2017 composer

これでPHPとComposerを使える環境が整いました。

php -v

composer -v

バージョンが返ってくればOK!

'****.dll' ** is not compatible with this PHP build linked with ** in Unknown on line 0

このようなエラーが出る方は、下記の記事が参考になります。

「MSVCR140.dll/VCRUNTIME140.dllがないため、プログラムが開始できません」と表示された場合

コマンドで操作を行うってやっぱり夢ありますよね。

これでバージョン管理も楽になるので、是非活用してください!

以上、「Windowsユーザー必見!Homebrewみたいにscoopを使ってコマンドからGit、Node、npm、PHPをダウンロードしてみよう!!」でした!

Thank you for reading

LIFFとStripe CheckoutでLINE上で使える決済画面を作るよ

$
0
0

LINE上でLINE Pay以外の方法で決済をしたいというクライアント様のご要望があり、LIFFとStripeを絡めたサンプルを作ってみました。この組み合わせはあまり記事が無かったので記録として残しておきます。

今回は、Stripeの決済にLINEのユーザーIDをメタデータとして乗せてみます。
StripeのGithubにあがっていたサンプルをLIFF向けに少し修正しました。

出来上がったもの

デモサイト
テスト用のカード4242-4242-4242-4242を使って決済の流れをご確認頂けます。カード有効期限とCVC、所有者名、メールアドレスは適当でokです。

Herokuの無料プランなので遅いです。LINEが立ち上がります

開発環境

Node.js v11.13.0

LINE側の設定

まずはLINE Developersからチャンネルを作成します。LINEログインのチャンネルにLIFFアプリを登録していきます。

認証するときに必要ですので、Scopeのopenidにチェックを入れることをお忘れなく。
エンドポイントURLはひとまず適当で大丈夫です(後から直せます)
image.png

チャネルIDLIFF IDを控えておいてください。
LIFF IDはLIFFアプリ作成後に付与されます。

Stripe側の設定

次にStripeのダッシュボードから定期支払いの商品を作成します。
image.png
詳細欄の商品ID(prod_XXXXXX)と料金欄の価格ID(price_XXXXXXXX)、ホーム画面で確認できる公開可能キー(pk_XXXXXXX)とシークレットキー(sk_XXXXXXX)を控えておいてください。

サーバーの設置

コードはGitHubのリポジトリを公開してますので、そちらを落としてみてください。

↓まずはプロジェクトの依存パッケージをダウンロードします

npm install

↓プロジェクト内のファイルを2か所、先ほど控えたご自身のIDに書き換えて頂く必要があります。
今回は使用しませんのでWebhook Secretはそのままで大丈夫です

.env

# Stripe keys
STRIPE_PUBLISHABLE_KEY=(Stripeの公開可能キー)
STRIPE_SECRET_KEY=(Stirpeのシークレットキー)

# Required to run webhook
# See README on how to use the Stripe CLI to setup
STRIPE_WEBHOOK_SECRET=whsec_1234

# Checkout options
DONATION_PRODUCT_ID=(作成した定期支払い商品のID)
SUBSCRIPTION_PRICE_ID=(価格ID)

# ex.https://amabot-payment.herokuapp.com
DOMAIN=(デプロイ先のドメイン)

# Environment variables 
STATIC_DIR=client

# LINE
CHANNEL_ID=(LINE Developersで設定したチャネルID)

client/liff.js (1行目)

constliffId=(作成したLIFFアプリのLIFFID)
LINEのユーザー情報の取り扱いに注意!!

クライアント側で取得したユーザー情報をサーバーに送ることは公式で禁止されています
今回はクライアント側でIDトークンを発行して、サーバーサイドでトークンの検証を行いユーザー情報を取得するようにしました。
https://developers.line.biz/ja/docs/liff/using-user-profile/
https://developers.line.biz/ja/reference/line-login/#verify-id-token

最後にLINE DevelopersのLIFFアプリ詳細からエンドポイントURLをご自身のものに書き換えます。
ここが厄介なのですが、LIFFアプリのエンドポイントURLはhttpsでないと登録できないのでご注意ください。
ローカルでデバックする際はngrok経由で動きを確認する方が多いようですね。
私はそのままherokuにデプロイしてしまいました。

なお、.envファイルのDOMAINの値もこちらと合わせてください。

サーバー起動

↓サーバーを起動します。

node server.js

LIFFアプリのURLを開くと決済画面が立ち上がるかと思います。

決済してからStripeのダッシュボードを確認してみると、、

20200728093917.jpg
処理が成功しています。メタデータのユーザーIDも登録されてました!

参考にしたサイト

あまり情報がなかったので公式ドキュメント中心です

https://developers.line.biz/ja/reference/line-login/
https://developers.line.biz/ja/docs/liff/using-user-profile/
https://stripe.com/docs/api/checkout/sessions/create
https://support.stripe.com/questions/using-metadata-with-checkout-sessions

node.jsで仮チャットサイトを作ってみる

$
0
0

初めに

TwitterのようなSNS系のサイトを作ってみたいと思っていたものの、まずは最初には何をすればいいのかと考えれば、何から手を付ければいいのかさっぱりわからなかったため、とりあえず仮でNode.jsのSocket.ioモジュールを利用して、チャットサイトを作ってみようと思います。

環境

・さくらVPSのCentOS7
・Node.js v15.11.0
・npm 7.6.2
・Mongodb 4.4.4

因みにですがさくらVPSの初期設定やnginxの設定までは載せません。そりゃそうですね。駅地図に火星の場所なんて書かないし。

プログラミングの前に

実際に作って見る前に必要な機能を考えてみましょう。
YouTubeやニコニコのコメントのことを参考にしてみれば、コメントをサーバー側に保存して後で見る人のために再生する機能が必要となります。また、ライブなどのリアルタイムであれば、即座に見ている全員にブロードキャストされているようです。
まとめると
・コメントのブロードキャスト
・コメントの保存機能
・コメントの再生機能
これらを作っていこうと思います。

モジュールのインストール

npm install socket.io
npm install mongodb
npm install express-generator -g

express-generator

コマンドラインで

express -e

と入力すればウェブサイトを作る前の雛形を作成することができます。お気に入りなので、とりあえずこれを元に作っていきます。
雛形に必要なモジュールは一緒にインストールされてるわけではないので、別途インストールを実行します。

npm install

公開は作成されたbinディレクトリのwwwファイルからすることができます。

node bin/www

としてChromeなどでアクセスしてみれば、デフォルトのHTMLが表示されます。

socket.io

bin/wwwファイルの中をいじっていきます。

node.js
/**
 * Create HTTP server.
 */varserver=http.createServer(app);すぐ下に追加vario=require("socket.io")(server);

別に必ずではないですが、今回はこの場所においておきましょう。

node.js
server.listen(port);server.on('error',onError);server.on('listening',onListening);すぐ下に追加io.on('connection',function(socket){socket.on('chat',function(data){io.emit('chat',{message:data});});});

サーバー側の設定はこれだけにしましょう。
次はviews/index.ejsのファイルをいじっていきます。

<!DOCTYPE html><html><head><title><%=title%></title><linkrel='stylesheet'href='/stylesheets/style.css'/><script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js"></script><script src="/socket.io/socket.io.js"></script><script>$(function(){varsocket=io.connect('http://133.167.44.189:80');//var socket = io.connect();$('#submit').click(function(){varmessage=$('#message');if(message[0]){socket.emit("chat",{message:message.val()})message.val("");}})socket.on("chat",function(data){appendMsg(data.message)});functionappendMsg(text){console.log(text);$("#chatLogs").append("<li>"+text+"</li>");}})</script></head><body><inputid="message"type="text"/><inputid="submit"type="submit"value="send"/><ulid="chatLogs"></ul></body></html>

機能美もデザイン性も考えずにとりあえずこうしましょう。
socket.emit(~)はデータをサーバー側に送信するためのもので、socket.onは逆にデータをサーバー側から受け取るためのものです。
さて、ここまでで一度動作の確認と行きましょう。

node bin/www

とexpress-generatorで作成したファイルをカレントディレクトリにして入力すれば、サーバーは起動します。IPアドレスなりドメインなりにchromeなどからアクセスすると
line_20210319_003701.png
シンプルオブベストな画面が表示されます。複数のタブでこのサイトを開いて、枠に文字列を打ち込んでsendのボタンを押せば、全部のタブにおいて共有されているのがわかります。リアルタイム性のあるブロードキャスト機能はこれで完成です。次は保存ですね。

mongodb

このままbin/wwwのファイルの中に直接mongodbについての情報を書き込んでもいいんですが、しかし如何せん長くなってしまうので別のファイルにわけて書こうと思います。
というわけでアクセスから何から何まで書こうと思ったのですが、それはそれで如何せん長くなってしまうのでチートシートをご用意しました!

https://qiita.com/SUNAsan/items/45cf7d8a2da0dabcce1b

まぁそれでは不親切すぎるので全コピーできるファイルもおいて置きましょう。

node.js
//./database/mongo_connect.jsconstMongoClient=require('mongodb').MongoClientfunctionopen(){leturl='mongodb://ユーザー名:パスワード@localhost:27017';constconnectOptions={useNewUrlParser:true,useUnifiedTopology:true,}returnnewPromise((resolve,reject)=>{MongoClient.connect(url,connectOptions,(err,db)=>{if(err)reject(err);elseresolve(db);});});}functionclose(db){if(db)db.close();}letdb={open:open,close:close}module.exports=db;
node.js
//./database/insertOne.jsconstzenodb=require('./mongo_connect.js');functioninsertOne(object,col){letdatabase=null;zenodb.open().then((db)=>{database=db;constdbName=db.db("自分のデータベース名を入力してね!");returndbName.collection(col);}).then((collection)=>{returncollection.insertOne(object);}).catch((err)=>{console.error(err)}).finally(()=>{zenodb.close(database);})}module.exports=insertOne;
node.js
//./database/methods.jsconstinsertOne=require("./insertOne.js");letmethods={insertOne:insertOne,}module.exports=methods;

こんな感じです。databaseディレクトリを作成して以上3つのファイルを作成しました。
編集する必要があるのは、最も最初にあるlet urlのユーザー名とパスワード、自分のデータベース名を入力してね!と書いてある場所ですね。
これらのファイルを使っていきます。
bin/wwwのファイルを編集します。

$vim bin/www
node.js
server.listen(port);server.on('error',onError);server.on('listening',onListening);すぐ下に追加varmethods=require("../database/methods.js");

requireをとりあえず追加しまして、先程作ったsocket.ioの中身も変えます。

node.js
io.on('connection',function(socket){socket.on('chat',function(data){methods.insertOne(data,"comments");io.emit('chat',data);});});

dataの構造はこれまで通りにしていれば、{message:"何かしらのコメント"}になっているはずなのでokです。これで保存機能は完成ですね。これも先程と同じように試してみてください。
次は再生機能です。

再生機能のためにfindでデータを探すメソッドを作る必要があります。

node.js
// ./database/findLimit.jsconstzenodb=require('./mongo_connect.js');functionfindLimit(col,limit){letdatabase=null;returnzenodb.open().then((db)=>{database=db;constdbName=db.db("自分のデータベース名を入力してね!");returndbName.collection(col);}).then((collection)=>{returncollection.find();}).then((results)=>{returnresults.limit(limit).toArray();}).catch((err)=>{console.error(err)}).finally(()=>{zenodb.close(database);})}module.exports=findLimit;

集合部分であるmethodsも編集します。

node.js
// ./database/methods.jsconstinsertOne=require("./insertOne.js");constfindLimit=require("./findLimit.js");letmethods={insertOne:insertOne,findLimit,findLimit,}module.exports=methods;

取得してきたコメントをhttpでクライアント側に反映させる必要があります。なので次に編集するのはroutes/index.jsです。

node.js
varexpress=require('express');varrouter=express.Router();varmethods=require('../database/methods');/* GET home page. */router.get('/',function(req,res,next){methods.findLimit("comments",20).then((results)=>{res.render('index',{title:'Express',datas:results});});});module.exports=router;

以上のように書き換えます。
resultsのデータ構造は[{id:,message:},{id:,message}]という風になっており、コメントを表示するというのはmessageだけが取得できればいいのですが、そのデータ処理は次の場所でします。ejsを使用するので、views/index.ejsを編集します。
上の方で一度作っていたHTMLのbodyを少し改変しただけです。

<body><inputid="message"type="text"/><inputid="submit"type="submit"value="send"/><ulid="chatLogs"><%for(dataofdatas){%><li><%=data.message%></li><%}%></ul></body>

以上の通りです。
これらすべてを作成し終えたところで

$node bin/www

と実行してみてアクセスしてみます。
適当にコメントをうってsendボタンを押してみて。
reloadをしてみれば。
見事に保存されているのがわかります!

今後

ここまで作ることができれば、あとはいろいろと要素を詰め込んで、デザインを変えていけばTwitterのようなSNSwebアプリケーションだってすぐです!夢が広がりますね。今後はSNSもうすこし機能を強化してみて、SNSとして特色をつけようとしたところだけ記事にしてみようと思います。
大事な部分だけを記事にしようと思います。今回の記事は少し長くしすぎた感がありますからね。(それに丁寧さがなかったから)
以上です。お疲れ様でした。

AWS for JavaScript で S3 の getObject で NoSuchKey を期待してたのに AccessDenied が返ってくる

$
0
0

問題

以下のようなコードコードを実行し

letprev=null;try{prev=awaitS3.getObject({Bucket:'my-bucket-hogehoge',Key:'path/to/object',}).promise();}catch(e){// オブジェクトが存在しない場合(NoSuchKey)は無視,それ以外は throw し直すif(e.code!=='NoSuchKey'){throwe;}}

存在しないオブジェクトへのアクセスは NoSuchKeyが返ってくるものと期待していて、s3rverを利用したローカル環境での動作確認はそれで問題なかった。

しかし、デプロイして実際にどうさせると AccessDenied が返ってくる。

原因

なんで?
と1時間くらい悩んだが SDK のドキュメントに書いてあった

https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#getObject-property

Permissions

You need the s3:GetObject permission for this operation. For more information, see Specifying Permissions in a Policy. If the object you request does not exist, the error Amazon S3 returns depends on whether you also have the s3:ListBucket permission.

  • If you have the s3:ListBucket permission on the bucket, Amazon S3 will return an HTTP status code 404 ("no such key") error.
  • If you don’t have the s3:ListBucket permission, Amazon S3 will return an HTTP status code 403 ("access denied") error.

s3:ListBucket Action が許可されていないと access deniedを返すと

対応

以下のようにポリシードキュメントで権限を付与したら解決。
Resource の指定方法が s3:getObjectと異なるので注意。

(一部抜粋){"Action":["s3:GetObject","s3:PutObject"],"Resource":"arn:aws:s3:::my-bucket-hogehoge/*","Effect":"Allow"},{"Action":["s3:ListBucket"],"Resource":"arn:aws:s3:::my-bucket-hogehoge","Effect":"Allow"}

初めてのLINEbot作成【Node.js + heroku】

$
0
0

はじめに

Qiita初投稿になります
Node.jsによるLINEbot製作を行ってみたく、自学のために行った流れをまとめていきます
Node.js、npmのインストール、Herokuのアカウント登録などは省きます
成果物としては、「ありがとう」と送信すると、「どういたしまして」、それ以外は「こんにちは」と返ってくるLINEbotを制作します

動作環境

・MacBook Pro (13-inch, 2019, Two Thunderbolt 3 ports)
・mac OS Catalina バージョン 10.15.7
・node v15.3.0
・npm 7.0.14
・git 2.23.0

目次

  1. LINE側の準備
  2. Node.js側の準備
  3. コードの作成
  4. 応答テスト
  5. 本番環境(heroku)にデプロイ
  6. 参考文献

LINE側の準備

この見出しでは、LINE側で行うことをまとめていきます。

LINE Developerアカウントの作成

まずはこちらのURLからLINE Developerのアカウントを作成します
普段使用されているLINEアカウントがあればすぐに作成できます
https://developers.line.biz/ja/

プロバイダーの作成

次にプロバイダーの作成を行います。トップページにある以下の作成ボタンを押します。
スクリーンショット 2021-03-19 17.28.36.png
ここでは新しく作るプロバイダーをtestとしておきます。
スクリーンショット 2021-03-19 17.29.43.png

チャネルの作成

新しくプロバイダを作成すると以下の画面に移行します。ここでは「Messaging API」を選択します。
スクリーンショット 2021-03-19 17.30.02.png
その後、チャネルの種類を「Messaging API」に指定し、プロバイダーを先ほど作成した「test」に指定します。その後
・チャネルアイコン ※一度変更すると、1日経過しなければ変更することができません
・チャネル名 ※一度変更すると、7日経過しなければ変更することができません
・チャネル説明
・大業種
・小業種
・メールアドレス
・プライバシーポリシーURL(任意)
・サービス利用規約URL(任意)
を入力し、利用規約にチェックを入れ、作成ボタンを押します
スクリーンショット 2021-03-19 17.32.20.png

チャネルシークレットとチャネルアクセストークンの取得

次に外部からLINE APIにアクセスするためのチャネルシークレットとチャネルアクセストークン(長期)を取得します。
こちらの情報は外部に漏れないように気を付けましょう。それぞれの場所は
・チャネルシークレット
 チャネル基本設定の下部にあります
・チャネルアクセストークン(長期)
 Messaging API設定の下部にあり、発行ボタンを押すと表示されます
上記二つが取得できたら、とりあえずはメモ帳等に記録しておきましょう。

その他設定

Messaging API設定タブの応答メッセージ、編集、にて応答設定の中の詳細設定の、応答メッセージを以下の画像のようにオフ、Webhookをオンにしましょう。あいさつメッセージはオフにしていますが、オンでも構いません。
※Webhook URLに関しては後ほど入力します
スクリーンショット 2021-03-19 17.59.27.png
こちらで一旦LINE側の準備は終わりになります。

Node.js側の準備

npm init

任意のフォルダ(ここではlinebot_test)を作成し、そのフォルダ内で、npmのイニシャライズを実行します

コンソール
mkdir linebot_test
cd linebot_test
npm init

npm init実行後、いろいろ聞かれますが、全て空のままEnterで構いません

モジュールのインストール

LINEbotを動かすために必要なnpmモジュールをそれぞれインストールします

コンソール
npm install express @line/bot-sdk dotenv --save

ローカルでテストするためのトンネリングツールをインストール

ngrokというトンネリングツールを使用することで、簡単にテストが行えます。

コンソール
npm install -g ngrok

Node側の準備はこのぐらいです、次からコードを書いていきます。

コードの作成

先ほど作成したフォルダ内で以下を実行し、index.jsを作成します

コンソール
touch index.js

その後、以下のコードをコピペします

index.js
// モジュールのインポートconstserver=require('express')();constline=require('@line/bot-sdk');// Messaging APIのSDKをインポートrequire('dotenv').config();// -----------------------------------------------------------------------------// パラメータ設定constlineConfig={channelAccessToken:process.env.LINE_ACCESS_TOKEN,// 環境変数からアクセストークンをセットしていますchannelSecret:process.env.LINE_CHANNEL_SECRET,// 環境変数からChannel Secretをセットしています};// -----------------------------------------------------------------------------// Webサーバー設定server.listen(process.env.PORT||3000);// -----------------------------------------------------------------------------// ルーター設定constbot=newline.Client(lineConfig);// APIコールのためのクライアントインスタンスを作成server.post('/webhook',line.middleware(lineConfig),async(req,res)=>{res.sendStatus(200);// 先行してLINE側にステータスコード200でレスポンスする。const[lineEvent]=req.body.events;// events配列から配列の0番目の要素だけを変数に代入constinputMessage=lineEvent.message.text;//入力された文字を代入if(!lineEvent){return;}if(lineEvent.type!=='message'||lineEvent.message.type!=='text'){return;}if(inputMessage==='ありがとう'){//「ありがとう」と入力された場合bot.replyMessage(lineEvent.replyToken,{type:'text',text:'どういたしまして',});} else{//それ以外の文字が入力された場合bot.replyMessage(lineEvent.replyToken,{type:'text',text:'こんにちは',});}});

そして環境変数のため、.envファイルを作成します。

コンソール
touch .env

.envファイル内は、LINE側の準備で取得したチャネルシークレットとチャネルアクセストークンを用意し、以下のコードに貼り付けてください

.env
LINE_ACCESS_TOKEN='メモしたチャネルアクセストークン'
LINE_CHANNEL_SECRET='メモしたチャネルシークレット'

フォルダ構成は以下のようになります
スクリーンショット 2021-03-19 18.48.35.png

応答テスト

いよいよテストになります。

LINEbotの友達登録

LINE DevelopeのMessaging API設定のボット情報にある「ボットのベーシックID」をコピーして、自分のLINEの友達検索、にてIDで検索すると作成したbotが出てくるので、そちらを友達にします

スクリーンショット 2021-03-19 23.28.47.png

このボット情報の下にQRコードが表示されているので、そちらから友達になっても構いません

ngrokの起動

コンソールを二つ立ち上げ、片方では

コンソール
ngrok http 3000

とすると、以下のようになるので

スクリーンショット 2021-03-19 18.58.29.png

https://~~~~.ngrok.io の部分をコピーし

LINE DevelopeのMessaging API設定のWebhook設定、webhook URLに貼り付け、後ろに「/webhook」を記入します

スクリーンショット 2021-03-19 19.02.24.png

index.jsの起動

もう一方のコンソールにて、

コンソール
node index.js

としてサーバーを起動します

応答テスト

いよいよテストです。
botに対して「ありがとう」と入力しましょう

すると、「どういたしまして」と返ってきます

それ以外の言葉を入力すると、「こんにちは」と返ってくるはずです。
スクリーンショット 2021-03-19 23.35.54.png

本番環境(heroku)にデプロイ

git init

herokuにデプロイする際はgitを使用するので、まずgit initを行います

コンソール
git init

git ignore

その後、チャネルアクセストークンなどが外部に漏れてしまわないように.gitignoreファイルを作成し、
以下のように記述しておきましょう

.gitignore
npm-debug.log
node_modules
.env

こうしておくことで、pushしても.envファイルが外部に漏れることはなくなります
また、node_modules npm-debug.logに関してもプッシュする必要がないのでignoreしておきましょう

heroku create

その後、herokuにログインし、heroku createを実施して新たにサーバーを作成しましょう
その後、herokuに環境変数を読ませるため、heroku config:setにて環境変数を定義しましょう

コンソール
heroku login
heroku create
heroku config:set LINE_ACCESS_TOKEN='メモしたチャネルアクセストークン'
heroku config:set LINE_CHANNEL_SECRET='メモしたチャネルシークレット'

Procfileの作成

次に、herokuがまず何を実行すればいいかを記したファイル「Procfile」を作成します

コンソール
touch Procfile

Procfile内は以下のように記します

Procfile
web: node index.js

デプロイ

いよいよデプロイです、以下のようにgit addを行い、git commitを行って、herokuにプッシュしましょう

コンソール
git add -A
git commit -m "コミット名"
git push heroku master

デプロイ後、LINE Developersのwebhook URLを変更

コンソールからherokuにデプロイを行うと、以下のようなログが流れます
スクリーンショット 2021-03-20 0.13.40.png
https://~~~~~.herokuapp.com/ の部分をコピーして、webhook URLに張り付けます

スクリーンショット 2021-03-20 0.16.48.png

この後、再度LINEbotにメッセージを送ってみましょう、localでテストしたときと同じ返答が帰ってくれば成功です

参考文献

discordjs/rpcを使ってPCのDiscordをスマホからミュートにしたい

$
0
0

動機

コロナ渦の大学生なので,ずっと家にいて暇です.常に仲間とDiscordのVCをつないで作業をしています.

ふとトイレに行くときに,PCのマイクをミュートにし忘れることがあります.
イヤホンはBluetoothなので音は聞こえているのですが,トイレとかキッチンに行った時とかにミュートにし忘れることがあって不便です.
そこで,離れたところからスマホでPCのDiscordをミュートにできたらと思っていたのですが,DiscordのRPCなAPIで実現できそうだったので試してみました.

環境

  • ThinkPad X270
  • Arch Linux
  • Node.js v14.16.0
  • discord-rpc v3.2.0
  • express
  • Discord

Discordのアプリを,Node.jsで動くエージェントサーバーで操作する感じです.
Node.jsのサーバー上で,ExpressによるWebサーバーとDiscordRPCクライアントが走っています.
Webサーバーがリクエストを受け取ったらDiscordのミュートをトグルする,といった形です.

準備

Discordの
- ClientID
- ClientSecret
- AccessToken

が必要になります.
AccessTokenのScopesについては,私の場合は,
- rpc
- rpc.api
- identify
のトークンを発行しました.

ソース

constDiscordRPC=require('discord-rpc')constexpress=require('express')constclientId='CLIENT_ID'constclientSecret='CLIENT_SECRET'if(process.argv.length<=2){console.error('You must specify access_token')return}constaccessToken=process.argv[2]constrpc=newDiscordRPC.Client({transport:'ipc'});functionstartWebServer(){constapp=express()constport=3000app.get('/',(req,res)=>{rpc.getVoiceSettings().then((e)=>{returnrpc.setVoiceSettings({mute:!e.mute})}).then(()=>{res.sendStatus(200)})})app.listen(port,'0.0.0.0',()=>{console.log(`Discord Muter listening at http://localhost:${port}`)})}rpc.on('ready',()=>{startWebServer()});rpc.login({clientId,scopes:['rpc','rpc.api','identify'],accessToken}).catch(console.error);

やってみよう

Discordのアプリを開いて,ボイスチャットに入っておきます.
$ node index.js {アクセストークン}でサーバーを起動します.
その状態で,普通に,PCのブラウザから,http://localhost:3000/にアクセスすると,ミュートになります.
もう一度アクセスでミュート解除です.

私はTaskerでウィジェット化しています.

他にもRPCにはいろいろなAPIが用意されていそうなので,時間があるときに探してみたいと思います!!


Nodeについて調べてみた

$
0
0

「JavaScript」の勉強をしているとターミナルにNode.jsに関して入力することがあったりしましたがそもそも「Nodeってなに?」と思ったので調べてみました。

Nodeとは

Nodeとは、節、結節(点)、節点、交点、中心点、集合点、こぶ、膨らみ、などの意味を持つ英単語。
ITの分野では網状構造の構成要素などをこのように呼ぶ。

複数の要素が結びついた構造体において、個々の要素のことをノードという。
ノードを結びつける線や繋がりは「エッジ」あるいは「リンク」という。

例えば [サーバ]ー[ルータ]ー[サーバ]などの構成のネットワークを点と線のみで結ぶと
●ー●ー●の点の部分(パソコン、ルータ、サーバ)が「ノード」

Node.jsとは?

サーバサイドで動くJavaScriptだと思っていましたが、
Node.jsはサーバサイドでJavaScriptを実行できるためのプラットフォームという理解が正しそう。

リアルタイムWebなどの分野でNode.jsが採用される事例(MicrosoftやYahoo等)がある
※リアルタイムWEB = リアルタイムな反応が必要な場所(FacebookやTwitter、Googleスプレッドシート等)

例えば、LAMP環境だと
・WEBサーバとWEBブラウザを常時接続する必要がある
・データを保存し続け、Webブラウザ側に表示し続ける必要がある

これら問題でチャット画面をみるのに必要以上に時間を要するらしいが、Node.jsではこれらの機能を比較的簡単なコードで実装できる。

参考にしたサイト

参考1 ( https://e-words.jp/w/%E3%83%8E%E3%83%BC%E3%83%89.html)
参考2 (https://wa3.i-3-i.info/word1300.html
参考3( https://eng-entrance.com/what-is-nodejs
参考4( http://www.tohoho-web.com/ex/nodejs.html
参考5( https://qiita.com/non_cal/items/a8fee0b7ad96e67713eb) 

TypeScript + express-graphql + TypeORM on Node.js ( for MySQL ) 環境を構築したった

$
0
0

はじめに

はじめまして。突然ですが、GraphQL、めちゃくちゃ良い技術です。
Rails に載った GraphQL を業務で使ってますが、フロントエンド開発がフッ軽になります。

もっと GraphQL に詳しくなりたい。でも、現在、フロントエンドエンジニアとして勤務中の私には、実務で GraphQL を触ることができたとしても せいぜい Type をちょろっと修正するくらい。

そこで、趣味で書いてる Vue.js 製 WEB アプリの API に GraphQL を採用することにしました。
導入から API として動かすところまでを勉強がてら実装したので、せっかくだし最小限の構成をご紹介します。備忘録も兼ねて。

・・・Rails に対してのモチベーションが高くない ☺️ ので、今回は express に載せてます。

実際にやってみた

TypeScript 使ってますが、サンプルコードの中では 面倒臭いので厳密に取り扱っていない箇所があります。そーりー。

下準備

package.json を用意

package.json
{"name":"graphql-on-express","dependencies":{"@types/cors":"^2.8.10","@types/express":"^4.17.11","@types/express-graphql":"^0.9.0","@types/mysql":"^2.15.18","@types/node":"^14.14.35","cors":"^2.8.5","express":"^4.17.1","express-graphql":"^0.12.0","graphql":"^15.5.0","mysql":"^2.17.1","typeorm":"^0.2.31","typescript":"^4.2.3"},"devDependencies":{"ts-node":"^9.1.1","tsconfig-paths":"^3.9.0"},}

DB まわりのアレコレは TypeORMというライブラリに任せます。

・・・package.json で足りない項目がある場合はテキトーに埋めてください 😇

TypeScript、TypeORM のコンフィグを用意

tsconfig.json
{"compilerOptions":{"sourceMap":false,"noImplicitAny":true,"module":"commonjs","target":"es5","lib":["es2018","dom"],"moduleResolution":"node","removeComments":true,"strict":true,"noUnusedLocals":true,"noUnusedParameters":false,"noImplicitReturns":true,"noFallthroughCasesInSwitch":true,"strictFunctionTypes":false,"baseUrl":"./","paths":{"@/*":["src/*"],},"emitDecoratorMetadata":true,"experimentalDecorators":true,},"include":["./src/**/*.ts"]}
ormconfig.json
{"type":"mysql","host":"Your DB endpoint","port":3306,"username":"Your DB username","password":"Your DB password","database":"Your DB name","synchronize":false,"logging":false,"entities":["src/database/entity/**/*.ts"],"migrations":["src/database/migration/**/*.ts"],"subscribers":["src/database/subscriber/**/*.ts"],"cli":{"entitiesDir":"src/database/entity","migrationsDir":"src/database/migration","subscribersDir":"src/database/subscriber"}}

node modules をインストール

$ npm i

データまわりの作業

Entity を用意

データベースとプログラムとの間でマッピングされたデータが、どのようなデータ構造をとるのかを定義します。
TypeORM では、このようなデータモデルを Entity という名称で表現するようです。
一般的な MVC フレームワークにおいて、Model と呼ばれているモノにイメージは近いですが、Model と違ってロジックを持たせることはあまり想定してなさ気です。だから、あくまでも "Model"じゃなくて ただの "Entity" (実体) なのかと思いました (小並感

src/database/entity/user.ts
import{Entity,BaseEntity,Column,PrimaryGeneratedColumn}from"typeorm";@Entity()exportclassUserextendsBaseEntity{@PrimaryGeneratedColumn('increment')id!:number;@Column({nullable:false})name!:string;}

サンプルなので、id と name というカラムだけをもったシンプルな構造を用意します。

マイグレーションする

なんと、TypeORM はマイグレーションの機能まで提供してくれています。ありがとう。

$ npx ts-node node_modules/.bin/typeorm migration:generate -n user

上記を実行すると、src/database/migration/xxxxxxxxxxxxx-user.tsというファイルが生成されます。

続けて、以下を実行します。

$ npx ts-node node_modules/.bin/typeorm migration:run

ずらずらと SQL の実行ログが流れ・・・

(省略)

Migration userxxxxxxxxxxxxx has been executed successfully.
query: COMMIT

最後にこんなログが出力されれば成功です。

ターミナルから MySQL に直接ログインできる方は実際にテーブルを確認してみてください。

(省略)

mysql> desc user;
+-------------+--------------+------+-----+---------+----------------+
| Field       | Type         | Null | Key | Default | Extra          |
+-------------+--------------+------+-----+---------+----------------+
| id          | int          | NO   | PRI | NULL    | auto_increment |
| name        | varchar(255) | NO   |     | NULL    |                |
+-------------+--------------+------+-----+---------+----------------+
2 rows in set (0.02 sec)

こんな感じになってるはず。

・・・テーブル名的には usersであってほしいけど、Entity を複数形にしなきゃいけないのかな 🤔

ロジックまわりの作業

Type を定義

src/schema/fields/user/types.ts
import{GraphQLObjectType,GraphQLNonNull,GraphQLString,GraphQLInt,GraphQLInputObjectType}from'graphql';exportconstUserType=newGraphQLObjectType({name:'User',fields:{id:{type:newGraphQLNonNull(GraphQLInt),},name:{type:newGraphQLNonNull(GraphQLString),}}});exportconstFetchUserInput=newGraphQLInputObjectType({name:'FetchUserInput',fields:{id:{type:newGraphQLNonNull(GraphQLInt),},}});exportconstCreateUserInput=newGraphQLInputObjectType({name:'CreateUserInput',fields:{name:{type:newGraphQLNonNull(GraphQLString),},}});

Schema を定義

実際に運用する際は、Entity に対して query と mutation があり、場合によっては、そこからさらにバリエーションが派生する、なんてこともあります。
ファイルを細かく分けていて、サンプルコードをみてるだけだと「冗長じゃね?」と思うかもしれませんが、上記の理由から処理が増えることを視野に入れてこうしてます。

src/schema/index.ts
import{GraphQLSchema}from"graphql";import{queryTypeasquery,mutationTypeasmutation}from"./fields";exportconstschema=newGraphQLSchema({query,mutation,});
src/schema/fields/index.ts
import{GraphQLObjectType}from'graphql';import{userField}from'./user';exportconstqueryType=newGraphQLObjectType({name:'Query',fields:{...userField.query,}})exportconstmutationType=newGraphQLObjectType({name:'Mutation',fields:{...userField.mutation,}})
src/schema/fields/user/index.ts
import{userQueryasquery}from'./query';import{userMutationasmutation}from'./mutation';exportconstuserField={query,mutation,};
src/schema/fields/user/query.ts
import{GraphQLNonNull}from'graphql';import*asresolversfrom'./resolvers';import{FetchUserInput,UserType}from'./types';constfetchUsers={type:UserType,args:{input:{type:newGraphQLNonNull(FetchUserInput)}},resolve:resolvers.fetchUsers,}exportconstuserQuery={fetchUsers,}
src/schema/fields/user/mutation.ts
import{GraphQLNonNull,GraphQLList}from'graphql';import*asresolversfrom'./resolvers';import{UserType,CreateUserInput}from'./types';constcreateUser={type:newGraphQLList(UserType),args:{input:{type:newGraphQLNonNull(CreateUserInput)}},resolve:resolvers.createUser}exportconstuserMutation={createUser,}

"DB への問い合わせ" や "結果を受け取って返却する" などのコアとなる処理を用意

src/schema/fields/user/resolvers.ts
import{User}from"@/database/entity/user"import{find,findOne,insert}from"../crud-assistant"// e.g.typeCreateUserArgs={input:{// any}}// e.g.typeFetchUserArgs={input:{// any}}// e.g.typeFetchUsersArgs={input:{// any}}exportconstcreateUser=async(args:CreateUserArgs):Promise<typeofUser>=>{returnnewPromise(async(resolve,reject)=>{constinsertInput=args.inputconstresult=awaitinsert<typeofUser,CreateUserArgs["input"]>(User,insertInput)if(!result){reject()return}constfindOneInput=args.inputconstuser=awaitfindOne<typeofUser,FetchUserArgs["input"]>(User,findOneInput)if(user){resolve(user)}else{reject()}})}exportconstfetchUsers=async(args:FetchUsersArgs):Promise<Array<typeofUser>>=>{returnnewPromise(async(resolve,reject)=>{constfindInput=args.inputconstusers=awaitfind<typeofUser,FetchUsersArgs["input"]>(User,findInput)if(users){resolve(users)}else{reject()}})}

CRUD の処理を共通化しておきます。
TypeORM の Repository のメソッドと同名で公開。

src/schema/fields/crud-assistant.ts
import{BaseEntity,createConnection,getRepository,InsertResult}from"typeorm"exportconstinsert=async<EextendstypeofBaseEntity,I>(Entity:E,input:I):Promise<InsertResult|null>=>{constconnection=awaitcreateConnection()constrepository=getRepository<E>(Entity)try{returnnewPromise(async(resolve,reject)=>{constresult=awaitrepository.insert({...input}).catch(async(e)=>{reject()})awaitconnection.close()resolve(result||null)})}catch(e){awaitconnection.close()returnPromise.resolve(e)}}exportconstfindOne=async<EextendstypeofBaseEntity,I>(Entity:E,input:I):Promise<E|null>=>{constconnection=awaitcreateConnection()constrepository=getRepository<E>(Entity)try{returnnewPromise(async(resolve,reject)=>{constresult=awaitrepository.findOne({...input}).catch(async(e)=>{reject()})awaitconnection.close()resolve(result||null)})}catch(e){awaitconnection.close()returnPromise.resolve(e)}}exportconstfind=async<EextendstypeofBaseEntity,I>(Entity:E,input:I):Promise<E[]|null>=>{constconnection=awaitcreateConnection()constrepository=getRepository<E>(Entity)try{returnnewPromise(async(resolve,reject)=>{constresult=awaitrepository.find({...input}).catch(async(e)=>{reject()})awaitconnection.close()resolve(result||null)})}catch(e){awaitconnection.close()returnPromise.resolve(e)}}

これでようやくロジックまわりのファイルが揃いました。

サーバまわりの作業

エントリポイントを用意

src/index.ts
import*asexpressfrom'express'import{graphqlHTTP}from'express-graphql'import*ascorsfrom'cors'import{schema}from'./schema'constport=4000constapp=express()app.use(cors())app.use(express.static('./'));app.use('/',graphqlHTTP({schema,graphiql:true}))app.listen(port,()=>{console.log(`Started server, http://localhost:${port}`)});

エントリポイントを用意したら、

$ npx ts-node -r tsconfig-paths/register src/index.ts

を実行。

これで、http://localhost:4000にアクセスすると Graph i QL という GUI が表示されるようになります。
API としてのリクエストには、POST を用います。

Vue アプリ側からは Vue Apollo 経由で API を call してます。その話はまたどこかでするかもしれないししないかもしれない。

おわりに

GraphQL について

冒頭でも触れましたが、フロントエンドの開発においてめちゃくちゃ便利です。
良い技術なわりに、あまり世に浸透していない気がする。もったいない 🥺

TypeORM について

便利ではありますが、提供してる型が微妙に扱いにくいと感じるところがあったり、(当たり前だけど) GraphQL 側にも型の指定が必要なので TypeORM ↔️ GraphQL 間で同じデータを指してるのに構造の定義が二重管理になっちゃったりと、小さな課題があるので要改善。暇なときに TypeORM 側のコードから GraphQL の Type 定義のコードを自動生成するようなスクリプトを書きたい。(すでに誰かが作ってるかも
まあ、使いこなせれば良きなライブラリな気はします。あと、日本語の文献が豊富ではないです。

おしまい。

Beginner's Series to: Node.js - 今から始める Node.js

$
0
0

新型コロナウイルスによるパンデミックが始まってかれこれ1年ちょっとが経過しましたが、皆さんいかがお過ごしでしょうか。

私はサンフランシスコに住んでいるのですが、もうずっと WFH (Working from home) 生活でオフィスのフリー・フードやドリンクが懐かしいというか、遠い昔のような気さえします。以前まで通っていたテック系企業が密集するオフィス街は今はまるでゴーストタウンのようになっています。本当に以前のような活気が戻ってくるのかはわかりません。

初心者向け Node.js 動画シリーズの日本語版を作ってみた

さて、私は猫 x2 🐈🐈‍⬛ に邪魔されつつ、この狭い自宅で毎日仕事をしているわけですが、時間がフレキシブルになった分、自由時間も多いので、髪を変な色に染めたりしていたのですが、もっと有意義なことをしたかったこともあり、マイクロソフトの同僚たちがやっていた Beginner's Series to: Node.jsプロジェクトに参加することにしました。私が参加した頃はすでにコンテンツもできあがっていて動画もほぼ取り終えていたので、私は日本語ローカライズ版をつくることにしました。

あいかわらず猫がふらふらしている部屋で素人のカメラ・ワークで撮った動画を、素人な動画編集で制作した、Microsoft の名前で出して問題ないのかという出来ではあるのですが、なんとか Beginner's Series to: Node.js - 今から始める Node.jsが完成しましたので、まさにこれから Node.js を学んでみたい、という方にみていただければ幸いです。

イントロ。

動画は全26話。そんなに見るヒマないわ!と言われそうなのですが、実際は1話がだいたい3分から6分程度の一口サイズになっていますので、比較的気楽に見れるかと思います。

Playlist はここから!
📺 Beginner's Series to: Node.js - 今から始める Node.js

途中から急に難しくなるかもしれません。私の滑舌もさほどよくなく、わかりにくい箇所もあるかと思われますので、キャプションで日本語をオンにして見ることをお勧めします。

キャプションを On にする

YouTube 画面の下にある ⚙️ (設定)アイコンをクリックして 字幕> 日本語(自動生成ではないほう)を選んでください。
YouTube 字幕設定
YouTube 字幕設定

これでより理解が深まるかと思います。

では、みなさん、よかったらみてみてください!
📺 Beginner's Series to: Node.js - 今から始める Node.jsScreen Shot 2021-03-22 at 4.53.31 PM.png

Alexaスキル開発チュートリアル(AWS Lamda, node.js)

$
0
0

Alexaスキル開発チュートリアル(AWS Lamda, node.js)

個人で開発した、Alexaスキル開発チュートリアルを記載します。

今回は自分の目標設定をAlexaに記憶させて、次回起動時に目標期限を自動で算出して教えてくれるAlexaスキルを開発してみようと思います。

完成イメージ

◼︎ 初回起動時
(自分)アレクサ、目標リマインダーを開いて
(Alexa)こんにちは、目標リマインダーへようこそ。目標達成希望日時を教えてください。
(自分)2020年10月9日
(Alexa)ありがとうございます。目標達成日時は2020年10月9日ですね。

◼︎ 次回起動時
(自分)アレクサ、目標リマインダー
(Alexa)おかえりなさい、次の目標達成日時まで1年、残り12日です。目標達成に向けて本日も頑張りましょう

前提知識

1. AWS Lambdaについての最低限の知識
2. JavsaScriptについての最低限の知識
3. Alexa(スマートスピーカー)についての最低限の知識

STEP1 「ログイン」

まず、Amazon開発者アカウントを使って Alexa開発者コンソールにログインします。アカウントがない場合は、こちらで作成します。

STEP2 「スキルを作成」

スクリーンショット 2021-03-09 10.49.47.png
コンソールの右側にあるスキルの作成をクリックします。新しいページが表示されます。

FireShot Capture 020 - Alexa Developer Console - developer.amazon.com.png

  1. スキル名を入力します。このスキル名が公開された時に表示される名前になります。
  2. スキルに追加するモデルを選択します。今回はオリジナルAlexaスキルを開発するのでカスタムを選択します。
  3. スキルのバックエンドリソースをホスティングする方法を選択します。今回はAlexaにホストを選択します。今回の開発言語はNode.jsにする。(AWSの無料枠には制限があるため、スキルの使用頻度が上がってきたら、独自にホスティングするオプションに変更するのがオススメ)

STEP3 「呼び出し名を作成」

呼び出し名とは自分が開発したAlexaスキルを呼び出すときに発生するワードになります。

アレクサ、目標リマインダーを開いて

この時の {{ 目標リマインダー }}が呼び出し名になります。

STEP4 「コードエディタでコードを書く」

image.png

コードエディタタブをクリックします。コードエディタでindex.jsファイルが開きます。

今回開発した「目標設定リマインダー」の完成コードを記載します。

全てのコードを詳しく解説すると、永遠に終わらないので簡単な解説コメントのみ記載します

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.
 * *//* *
* ハンドラー関数について
* スキルがリクエストを受け取ると、各ハンドラーのcanHandle() 関数がそのハンドラーでリクエストに対応できるかを判断します。
* canHandle() 関数 -> ハンドラーが応答するリクエストを定義します(trueを返すことで、この処理を実行できる)
* handle() 関数 -> ユーザーに応答する内容を定義します
* const Alexa = require('ask-sdk-core');
* const persistenceAdapter = require('ask-sdk-s3-persistence-adapter');
* */constLaunchRequestHandler={canHandle(handlerInput){returnAlexa.getRequestType(handlerInput.requestEnvelope)==='LaunchRequest';},handle(handlerInput){//  起動時の挨拶constspeakOutput='こんにちは、目標リマインダーへようこそ。目標達成希望日時を教えてください。';// ユーザーが答えなかった場合に、もう一度たずねるconstrepromptOutput='私の目標達成日時は2020年10月1日です、あなたの目標達成希望日時を教えてください。';returnhandlerInput.responseBuilder.speak(speakOutput).reprompt(repromptOutput).getResponse();}};constHasDreamdayLaunchRequestHandler={// canHandle()関数はユーザーの目標達成日時がAmazon S3に保存されているかどうかをチェックします// 保存されていれば、ハンドラーはSDKに続行が可能である(ユーザーの誕生日が保存されているため、次の処理に進めると知らせます。// 保存されていない場合はLaunchRequestHandlerを呼び、目標達成日時の取得を行うcanHandle(handlerInput){constattributesManager=handlerInput.attributesManager;constsessionAttributes=attributesManager.getSessionAttributes()||{};constyear=sessionAttributes.hasOwnProperty('year')?sessionAttributes.year:0;constmonth=sessionAttributes.hasOwnProperty('month')?sessionAttributes.month:0;constday=sessionAttributes.hasOwnProperty('day')?sessionAttributes.day:0;returnhandlerInput.requestEnvelope.request.type==='LaunchRequest'&&year&&month&&day;},// handle()関数は、「おかえりなさい。Y歳の誕生日まであとX日です。」と言うようAlexaに指示します。// APIを使ってタイムゾーンを取得するため、応答を取得するまでに時間がかかる可能性がるので非同期処理にするasynchandle(handlerInput){//  ServiceClientファクトリーを作成constserviceClientFactory=handlerInput.serviceClientFactory;//  デバイスIDを取得constdeviceId=handlerInput.requestEnvelope.context.System.device.deviceId;constattributesManager=handlerInput.attributesManager;constsessionAttributes=attributesManager.getSessionAttributes()||{};constyear=sessionAttributes.hasOwnProperty('year')?sessionAttributes.year:0;constmonth=sessionAttributes.hasOwnProperty('month')?sessionAttributes.month:0;constday=sessionAttributes.hasOwnProperty('day')?sessionAttributes.day:0;// タイムゾーン取得、タイムゾーン取得失敗の時はエラーログ吐くletuserTimeZone;try{constupsServiceClient=serviceClientFactory.getUpsServiceClient();userTimeZone=awaitupsServiceClient.getSystemTimeZone(deviceId);}catch(error){if(error.name!=='ServiceError'){returnhandlerInput.responseBuilder.speak("サービスへの接続がうまく行きませんでした。").getResponse();}console.log('error',error.message);}// 現在の日付と時刻を取得しますconstcurrentDateTime=newDate(newDate().toLocaleString("ja-JP",{timeZone:userTimeZone}));// 日数計算の結果に影響するため、日付から時刻を取り除きますconstcurrentDate=newDate(currentDateTime.getFullYear(),currentDateTime.getMonth(),currentDateTime.getDate());// 現在年度を取得constcurrentYear=currentDate.getFullYear();// 目標達成日時の取得// todo: 変数名変更letnextBirthday=Date.parse(`${month}${day}, ${currentYear}`);// 目標達成日時が今年でなければ何年先かを求めるif(currentDate.getTime()>nextBirthday){consttermDay=(currentDate.getTime()-nextBirthday)/86400000;if(termDay>365){consttermYear=Math.floor(termDay/365)nextBirthday=Date.parse(`${month}${day}, ${currentYear+termYear}`);}}// 8640000(1日をミリ秒に換算したもの)constoneDay=24*60*60*1000;// 目標達成日時の場合はおめでとうと発生するletspeechText=`本日が目標達成日時になります。目標達成おめでとうございます。`;if(currentDate.getTime()!==nextBirthday){constdiffDays=Math.round(Math.abs((currentDate.getTime()-nextBirthday)/oneDay));speechText=`おかえりなさい、次の目標まで${currentYear-year}年、残り${diffDays}日です。目標達成に向けて本日も頑張りましょう`}returnhandlerInput.responseBuilder.speak(speechText).getResponse();}};constCaptureGoalIntentHandler={canHandle(handlerInput){returnhandlerInput.requestEnvelope.request.type==='IntentRequest'&&handlerInput.requestEnvelope.request.intent.name==='CaptureGoalIntent';},// 非同期処理で必要情報をs3保存asynchandle(handlerInput){constyear=handlerInput.requestEnvelope.request.intent.slots.year.value;constmonth=handlerInput.requestEnvelope.request.intent.slots.month.value;constday=handlerInput.requestEnvelope.request.intent.slots.day.value// ユーザーの目標達成日時を保存するconstattributesManager=handlerInput.attributesManager;// Amazon S3に値を保存する項目設定constdreamDayAttributes={"year":year,"month":month,"day":day};// ユーザーの情報がAmazon S3に送信されるまで実行完了を待つattributesManager.setPersistentAttributes(dreamDayAttributes);awaitattributesManager.savePersistentAttributes();constspeakOutput=`ありがとうございます。目標達成日時は${year}${month}${day}日ですね。`;returnhandlerInput.responseBuilder.speak(speakOutput)//.reprompt('add a reprompt if you want to keep the session open for the user to respond').getResponse();}};// ユーザ発話:「ヘルプ」と発した時の処理constHelpIntentHandler={canHandle(handlerInput){returnAlexa.getRequestType(handlerInput.requestEnvelope)==='IntentRequest'&&Alexa.getIntentName(handlerInput.requestEnvelope)==='AMAZON.HelpIntent';},handle(handlerInput){constspeakOutput='あなたの目標達成日時を教えてくれると私が目標達成日時までの残り時間を計算します。あなたの目標達成希望日時を教えてください。';returnhandlerInput.responseBuilder.speak(speakOutput).reprompt(speakOutput).getResponse();}};// ユーザ発話:「キャンセル」と発した時の処理constCancelAndStopIntentHandler={canHandle(handlerInput){returnAlexa.getRequestType(handlerInput.requestEnvelope)==='IntentRequest'&&(Alexa.getIntentName(handlerInput.requestEnvelope)==='AMAZON.CancelIntent'||Alexa.getIntentName(handlerInput.requestEnvelope)==='AMAZON.StopIntent');},handle(handlerInput){constspeakOutput='さようなら〜';returnhandlerInput.responseBuilder.speak(speakOutput).getResponse();}};/* *
 * FallbackIntent triggers when a customer says something that doesn’t map to any intents in your skill
 * It must also be defined in the language model (if the locale supports it)
 * This handler can be safely added but will be ingnored in locales that do not support it yet 
 * */constFallbackIntentHandler={canHandle(handlerInput){returnAlexa.getRequestType(handlerInput.requestEnvelope)==='IntentRequest'&&Alexa.getIntentName(handlerInput.requestEnvelope)==='AMAZON.FallbackIntent';},handle(handlerInput){constspeakOutput='Sorry, I don\'t know about that. Please try again.';returnhandlerInput.responseBuilder.speak(speakOutput).reprompt(speakOutput).getResponse();}};// ユーザ発話:「終了」と発した時の処理constSessionEndedRequestHandler={canHandle(handlerInput){returnAlexa.getRequestType(handlerInput.requestEnvelope)==='SessionEndedRequest';},handle(handlerInput){console.log(`~~~~ Session ended: ${JSON.stringify(handlerInput.requestEnvelope)}`);// Any cleanup logic goes here.returnhandlerInput.responseBuilder.getResponse();// notice we send an empty response}};// ユーザ発話:「<カスタムインテントのサンプル発話(デバッグ用)>」constIntentReflectorHandler={canHandle(handlerInput){returnAlexa.getRequestType(handlerInput.requestEnvelope)==='IntentRequest';},handle(handlerInput){constintentName=Alexa.getIntentName(handlerInput.requestEnvelope);constspeakOutput=`You just triggered ${intentName}`;returnhandlerInput.responseBuilder.speak(speakOutput)//.reprompt('add a reprompt if you want to keep the session open for the user to respond').getResponse();}};// エラーハンドラconstErrorHandler={canHandle(){returntrue;},handle(handlerInput,error){constspeakOutput='すいません、ちょっと何言ってるかわからないです、もう一回話しかけてください';console.log(`~~~~ Error handled: ${JSON.stringify(error)}`);returnhandlerInput.responseBuilder.speak(speakOutput).reprompt(speakOutput).getResponse();}};// S3保存したデータを読み込む// Alexaがユーザーに目標日時をたずねる前にAmazon S3に保存したデータを読み込むconstLoadDreamdayInterceptor={asyncprocess(handlerInput){constattributesManager=handlerInput.attributesManager;constsessionAttributes=awaitattributesManager.getPersistentAttributes()||{};constyear=sessionAttributes.hasOwnProperty('year')?sessionAttributes.year:0;constmonth=sessionAttributes.hasOwnProperty('month')?sessionAttributes.month:0;constday=sessionAttributes.hasOwnProperty('day')?sessionAttributes.day:0;// データが存在-> s3に保存されている日時を取得してセットするif(year&&month&&day){attributesManager.setSessionAttributes(sessionAttributes);}}};/**
 * 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().withApiClient(newAlexa.DefaultApiClient())// Amazon S3にデータを保存、読み込む設定.withPersistenceAdapter(newpersistenceAdapter.S3PersistenceAdapter({bucketName:process.env.S3_PERSISTENCE_BUCKET}))// 関数を呼び出す順に羅列する.addRequestHandlers(HasDreamdayLaunchRequestHandler,LaunchRequestHandler,CaptureGoalIntentHandler,HelpIntentHandler,CancelAndStopIntentHandler,FallbackIntentHandler,SessionEndedRequestHandler,IntentReflectorHandler)// インターセプターを登録するコードを追加して、SDKにその存在を知らせる.addRequestInterceptors(LoadDreamdayInterceptor)// エラーハンドリング.addErrorHandlers(ErrorHandler).withCustomUserAgent('sample/hello-world/v1.2').lambda();
package.json
{"name":"dream-time","version":"1.2.0","description":"alexa utility for quickly building skills","main":"index.js","scripts":{"test":"echo \"Error: no test specified\"&& exit 1"},"author":"Amazon Alexa","license":"Apache License","dependencies":{"ask-sdk-core":"^2.7.0","ask-sdk-model":"^1.19.0","aws-sdk":"^2.326.0","ask-sdk-s3-persistence-adapter":"^2.0.0"}}

STEP5 「インテントとスロットを使って情報を取得する」

ユーザーがAlexaの質問にどう答えるかを解釈するインテントを作成します。

インテントとは、ユーザーの音声によるリクエストを満たすアクションのことです。

インテントでは、スロットという引数を任意で使用することもできます。

インテント追加方法

image.png

カスタムインテントを作成を選択し、インテント名としてCaptureGoalIntentを入力し、カスタムインテントを作成をクリックすると新規でインテントを作成できます。

CaptureGoalIntentはindex.jsで定義したインテント名になります。(これは自分で定義したカスタムインテントなので追加する必要があります)

スクリーンショット 2021-03-13 14.25.06.png

スロット作成方法

次にスロットを作成しましょう。

今回の目標達成日時では、年、月、日という3つの重要な情報を収集します。

これらの情報をスロットと呼びます。Alexaにどの単語がスロットで、どのタイプのスロットなのかを知らせる必要があります。

私は2021年11月7日までに目標を達成します

人によって、動的に変更される箇所を波括弧({ })で囲んで適切な変数名に変更します。

私は{year} 年{month}月{day}日までに目標を達成します

スクリーンショット 2021-03-13 14.31.57.png

スロットタイプドロップダウンメニューから、各スロットにスロットタイプを割り当てます。
image.png

必須のスロットには必須の設定をしましょう。

スロットに右側にある「ダイアログを編集」をクリックし、下記のように必須のチェックボックスをONにします。

Alexaの音声プロンプトにはユーザーがmonthスロットの値を提供しなかった場合にAlexaが言うテキストを入力します。

ユーザーの発音にユーザーが想定する発話を入力します。

スクリーンショット 2021-03-13 14.32.43.png

STEP6 「テストする」

最後の実際にスキルが正常に動くかテストしてみましょう。

テスト タブをクリックします。テストシミュレーターが開きます。
image.png

するとAlexaシュミレータが表示されるので、サンプル発話を入力していきましょう。(赤枠の箇所に入力もしくは発話でinputします)

1. まず呼び出し名の「目標リマインダー」と発話する
2. アレクサスキルが挨拶と、質問をしてくる
3. 目標日時を発話する
4. アレクサスキルが目標達成日時を記憶する。

次に再度、アクレさを起動させたときに以前に記憶させた日時を記憶しており、現在日時から目標達成日時までの残り期間を自動計算してレスポンス返却してくれるかを確認します。

もう一度、アレクサを呼び出すと目標達成日時までの残り期間を自動計算してレスポンス返却成功です。

スクリーンショット 2021-03-13 14.55.59.png

STEP7 「スキル公開」

ユーザーへの公開前にいくつかのステップを実施する必要があります。まず、Alexa開発者コンソールの公開タブで、必要な情報を入力します。

スキルのプレビュー設定

Alexaスキルストアにスキルがどう表示されるかに関する情報を入力します。(下記図参照)
image.png

プライバシーとコンプライアンス設定

プライバシーとコンプライアンス設定を行います(下記図参照)
image.png

公開範囲

Alexaスキルをどの範囲まで公開するかの設定を行います(下記図参照)
スクリーンショット 2021-03-14 17.08.09.png

STEP8 「スキルの認定と公開」

公開範囲の設定が終われば、実際にスキルを申請してみましょう。

申請が完了したた下記図のようにステータスが申請中になります。

結果は数日後にメールにて連絡がきます。
スクリーンショット 2021-03-20 21.45.23.png

申請に落ちた場合

審査に落ちた場合はAmazonより審査に落ちた理由をめちゃくちゃ丁寧に教えてくれるメールを送ってくれます。

下記の写真は自分が審査に落ちたときに送られてきたメールになります。(すごく丁寧に落ちた理由を教えてくれました)

指摘箇所を直し再審査可能なので、修正でき次第また審査に応募しましょう。

スクリーンショット 2021-03-20 21.48.06.png

申請に合格した場合

スキルのステータスや、認証審査プロセスに関する最新情報は、認定>申請から確認できます。

申請に合格すると、ステータスが「認定済み」に変更されます。

スクリーンショット 2021-03-23 12.26.22.png

また、スキルが公開される時期の目安がEメールで通知されます。(公開時期を後から設定にした場合は公開時間を設定してくださいと表記される)
スクリーンショット 2021-03-23 12.29.47.png

まとめ

今回はAWSアレクサの開発から公開までの流れをご紹介しました。

普段、利用しているAIスピーカーが裏側でどのように開発、運用されているかの勉強になりました。

興味ある方はぜひ開発に挑戦してみてください。

git mv で expressアプリケーションのディレクトリを移動しようとしたらpermission deniedでつまづいた話

$
0
0

問題

expressアプリケーションのディレクトリ構成を変更するため、
git mvでディレクトリを移動しようしたが
permission deniedにより移動できなかった。
パーミッションを確認したが問題はなかった。

解決方法

node_modulesを含むディレクトリをgit mvで移動しようとすると
permission deniedエラーになるため、
一度node_modulesを削除してからgit mvで移動する。
最後にyarn installでnode_modulesを復元する。

くわしく

.
├── express_nextjs
│   └── app
│        ├── express
│        │     ├── node_modules
│        │     └── その他 
│        └── Dockerfile
│
└── その他

.
├─ app
│   ├── express
│   │     ├── node_modules
│   │     └── その他 
│   └── Dockerfile
│
└── その他

にするためアプリケーションルートにて

$ git mv ./express_nextjs/app ./app

を試みたがpermission denied

$ mv ./express_nextjs/app ./app

permission denied

ls -lでパーミッションを確認したが問題なし。

node_modulesを一旦削除する

git rm -rf ./express_nextjs/app/express/node_modules

再度

$ git mv ./express_nextjs/app ./app

でディレクトリを移動することに成功した。

最後にyarn installnode_modulesを復元した。

Viewing all 8823 articles
Browse latest View live