Fablog

社会人マイナス1年生のブログ / プログラミング / 料理 / ビットコイン

actions SDK Carousel のサンプルコード

github.com

Actions SDK を使っている時にカルーセルの使い方がわからなかったので色々調べていた

結論としては使い方とか全部ライブラリのコードにコメントアウトで書いてあったのだけど

いつかそのことを忘れてググってもすぐでてくるようにメモ

const app = new ApiAiApp({request, response});
const WELCOME_INTENT = 'input.welcome';
const OPTION_INTENT = 'option.select';

function welcomeIntent (app) {
  app.askWithCarousel('Which of these looks good?',
    app.buildCarousel()
     .addItems([
       app.buildOptionItem(SELECTION_KEY_ONE,
         ['synonym of KEY_ONE 1', 'synonym of KEY_ONE 2'])
         .setTitle('Number one'),
       app.buildOptionItem(SELECTION_KEY_TWO,
         ['synonym of KEY_TWO 1', 'synonym of KEY_TWO 2'])
         .setTitle('Number two'),
     ]));
}

function optionIntent (app) {
  if (app.getSelectedOption() === SELECTION_KEY_ONE) {
    app.tell('Number one is a great choice!');
  } else {
    app.tell('Number two is a great choice!');
  }
}

const actionMap = new Map();
actionMap.set(WELCOME_INTENT, welcomeIntent);
actionMap.set(OPTION_INTENT, optionIntent);
app.handleRequest(actionMap);

actions SDK GoogleHome と スマホ で処理を分けたくなった時に使うTIPS

github.com

actions sdk を使っている時、GoogleHome と スマホ で処理を分けたくなった時に使うTIPS

こんな感じで処理を記述すると、画面の有無を取得することができるので処理を分けることができる

if (app.hasSurfaceCapability(app.SurfaceCapabilities.SCREEN_OUTPUT)) {
  smart phone 用の処理
} else {
  google home 用の処理
}

GoogleAssistantアプリを作るチュートリアル的なものを作りたかった

概要

GoogleAssistantアプリの開発プラットフォームがついに日本語にも対応しましたね

developers.googleblog.com

せっかくなので簡単なアプリをリリースしました

そしてその作ったアプリの過程をまとめて、GoogleAssistantアプリを作るチュートリアルを作りました。。。。

と言いたかったのですがチュートリアルとしては微妙なものができてしまいました

しっかりした解説はないけど、GoogleAssistantアプリを作ったことないけど作りたい人には役立つと思うのでよかったら読んでください

Projectの作成

Actions on Google の Console に行って add Project をします

https://console.actions.google.com/

するとProjectNameとCountryを選ぶ箇所があるので選んでCreateProjectを押してください

するとこんな感じで API.AI を使うか Actions SDK を使うかみたいに選ぶ画面になるので選んでください 今回はAPI.AIを使って行くのでAPI.AIを選んでください

ここで選んだプラットフォームは取り消せないので、間違えて選んでしまった人は新しくプロジェクトを作り直してください

API.AIを選ぶとこんな感じでAPI.AIのプロジェクトを作れよと言われるので作ります

API.AIの画面に行くとdescription, language, timezone などを設定する場所があります。Alarmとかみたいなものを選択できる箇所もあるのですが、日本語は対応していないので、languageで日本語を選択するとその箇所は消えるので埋めなくても大丈夫です。

選択して保存するとこんな感じになります

DefaultIntent

まずは最初に呼ばれるDefaultWelcomeIntentというトリガー的なものを設定します 左のIntentsを選んだあとに default welcome intent を選んでください

その後、GoogleAssistantで起動された時にこのdefault welcome intent が起動するように、Eventsのところに Google_ASSISTANT_WELCOME を入力してください

次に受け答えを設定します UserSaysのところに「こんにちは」 textResponseのところに「こんにちは!フリーブックスです!」 と入力してください

ここで、一度動くかテストをしてみましょう 画面の右端にある Try it now に「こんにちは」と入力してみてください

すると先ほど入力しように「こんにちは!フリーブックスです!」とのレスポンスが返ってきます。

このように、API.AIではユーザーの入力とそのレスポンスを事前に定義しておくことでBotを作ることができます。

しかし、この侭では事前にレスポンスを全て用意しておかなければならず大変です。 そこでAPI.AIのwebhook機能を使います。

CloudFunction の利用

ここで一旦API.AIはおいて、cloudfunctionに移ります

Actions on Google を参考にして ApiAiApp を作って行きます

Build Fulfillment  |  Actions on Google  |  Google Developers

fablog.hatenadiary.com

コードを貼るので詳しい説明は省きますが、基本的にやっていることは

  • appを引数に取るIntentメソッドを作る
  • Intentメソッドの中でいい感じにレスポンス文言を返すようにする
  • Actionと呼ばれる、後々API.AIで定義する文字列とIntentメソッドを関連づける

というだけです。 GoogleHome とそれ以外の端末でレスポンスを分けたりとか、カルーセルの使い方とかを知りたいって方はここを読んでください

fablog.hatenadiary.com

'use strict';
/*
* Modules
*/
const App = require('actions-on-google').ApiAiApp;
const functions = require('firebase-functions');
const request = require('request');
const cheerio = require('cheerio')

/*
* Constants
* 定数系
*/
const BASE_URL = 'https://www.amazon.co.jp/';
const Status = {
welcome: 0
, showBooks: 1
, selectedBook: 2
}

const Actions = {
WELCOME: 'welcome',
SHOW_BOOKS: 'show.books',
CONFIRM: 'confirm',
SELECTED_BOOK: 'selected.book',
}

let options = {
method: 'GET',
url: BASE_URL,
encoding: 'UTF-8',
headers: {
'Content-Type': 'text/plain;charset=utf-8',
'User-Agent': 'Mozilla/5.0(Linux;Android6.0;Nexus5Build/MRA58N)AppleWebKit/537.36(KHTML,likeGecko)Chrome/61.0.3163.100MobileSafari/537.36'
}
}

const books = [{ url: 'https://www.amazon.co.jp//gp/aw/d/B00BB1ZSJA?ref_=msw_best_freehome_0&storeType=ebooks',
title: 'レッツ☆ラグーン(1) (ヤングマガジンコミックス)',
image: 'https://images-fe.ssl-images-amazon.com/images/I/51tbQJsZxLL._BG0,0,0,0_FMpng_SX300_.jpg' },
{ url: 'https://www.amazon.co.jp//gp/aw/d/B075NKCVKD?ref_=msw_best_freehome_1&storeType=ebooks',
title: 'ゴールデンカムイ【期間限定無料】 1 (ヤングジャンプコミックスDIGITAL)',
image: 'https://images-fe.ssl-images-amazon.com/images/I/618tG-9ZD0L._BG0,0,0,0_FMpng_SX300_.jpg' },
{ url: 'https://www.amazon.co.jp//gp/aw/d/B00BML5V6E?ref_=msw_best_freehome_2&storeType=ebooks',
title: '亜人(1) (アフタヌーンコミックス)',
image: 'https://images-fe.ssl-images-amazon.com/images/I/51VKi59sPPL._BG0,0,0,0_FMpng_SX300_.jpg' },
{ url: 'https://www.amazon.co.jp//gp/aw/d/B075NL61FD?ref_=msw_best_freehome_3&storeType=ebooks',
title: 'ゴールデンカムイ【期間限定無料】 2 (ヤングジャンプコミックスDIGITAL)',
image: 'https://images-fe.ssl-images-amazon.com/images/I/61mxKIeQYDL._BG0,0,0,0_FMpng_SX300_.jpg' },
{ url: 'https://www.amazon.co.jp//gp/aw/d/B075TY9SD3?ref_=msw_best_freehome_4&storeType=ebooks',
title: 'アヤメくんののんびり肉食日誌(1)【期間限定 無料お試し版】 (FEEL COMICS)',
image: 'https://images-fe.ssl-images-amazon.com/images/I/513a5UTtm9L._BG0,0,0,0_FMpng_SX300_.jpg' },
{ url: 'https://www.amazon.co.jp//gp/aw/d/B00KCL6Y2U?ref_=msw_best_freehome_5&storeType=ebooks',
title: 'いぬやしき(1) (イブニングコミックス)',
image: 'https://images-fe.ssl-images-amazon.com/images/I/51YEu609hIL._BG0,0,0,0_FMpng_SX300_.jpg' },
{ url: 'https://www.amazon.co.jp//gp/aw/d/B075J8HKBZ?ref_=msw_best_freehome_6&storeType=ebooks',
title: 'うわばみ彼女【期間限定無料版】 1 (ジェッツコミックス)',
image: 'https://images-fe.ssl-images-amazon.com/images/I/51S8gTApfPL._BG0,0,0,0_FMpng_SX300_.jpg' },
{ url: 'https://www.amazon.co.jp//gp/aw/d/B00UN48X98?ref_=msw_best_freehome_7&storeType=ebooks',
title: '本当にあった笑える話【無料連載版】',
image: 'https://images-fe.ssl-images-amazon.com/images/I/61XtF2XgZxL._BG0,0,0,0_FMpng_SX300_.jpg' },
{ url: 'https://www.amazon.co.jp//gp/aw/d/B00DW4ZYBG?ref_=msw_best_freehome_8&storeType=ebooks',
title: '宝石の国(1) (アフタヌーンコミックス)',
image: 'https://images-fe.ssl-images-amazon.com/images/I/61FXZ-+RZLL._BG0,0,0,0_FMpng_SX300_.jpg' }]

/*
* Model
* sessionの間値を維持しておくための仕組み
*/
function initData(app) {
let data = app.data;
if(!data.books) {
data.books = {
data: [],
page: 0
}
}

if(!data.status) {
data.status = Status.welcome
}

return data;
};


/*
* Intents
* API.AI 呼び出されるIntent
*/
function showBooksIntent(app) {
console.log('called showBooksIntent action')
initData(app)
app.data.status = Status.showBooks
if (app.hasSurfaceCapability(app.SurfaceCapabilities.SCREEN_OUTPUT)) {
console.log('*** has screen ***')
let response = '本日のおすすめの無料コミックはこちらです。'
const items = books.map((book) => {
const item = app.buildOptionItem(book.title, book.title)
item.setTitle(book.title)
item.setImage(book.image, book.title)
return item
})

const carousel = app.buildCarousel().addItems(items)
app.data.books.data = books
app.askWithCarousel(response, carousel)
return

} else {
console.log('*** do not has screen ***')
const offset = i * 3
if(offset + 3 > books.length) return app.tell('申し訳ございません。おすすめコミックが見つかりませんでした。')
let response = '本日無料のおすすめコミックは「' + books[offset] + '」「' + books[offset + 1] + '」「' + books[offset+ 2] + '」'
response += '他のコミックを探しますか?'
app.ask(response);
return
}
}

function confirmIntent(app) {
console.log('called showMoreIntent action')
initData(app)

if(app.data.status == Status.showBooks) {
app.data.books.page += 1
showBooksIntent(app)
return
} else {
app.tell('申し訳ございません。よくわかりませんでした。');
}
}

function selectedBookIntent(app) {
console.log('called selectedBookIntent action')
initData(app)
app.data.status = Status.SELECTED_BOOK

const book = app.data.books.data.find(book => {
return book.title == app.getSelectedOption()
})

if(!book) return app.tell('申し訳ございません。通信に失敗致しました。しばらくしてからもう一度御利用ください。');

const response = app.buildRichResponse()
response.addSimpleResponse(book.title + 'ですね')
const card = app.buildBasicCard(book.title)
.setTitle(book.title)
.setImage(book.image, book.title)
.addButton('コミックを入手する', book.url)

response.addBasicCard(card)
app.tell(response)
}

/*
* Mapping
* 実際に cloud functions に公開するメソッド
* API.AIからwebhookでこのメソッドが毎回呼ばれる
* Actions on Google SDK を使ってactions名とIntentメソッドの紐付けを行う
*/
const Functions = functions.https.onRequest((request, response) => {
const app = new App({request, response});
console.log('Request headers: ' + JSON.stringify(request.headers));
console.log('Request body: ' + JSON.stringify(request.body));

const actionMap = new Map();
actionMap.set(Actions.WELCOME, showBooksIntent);
actionMap.set(Actions.SHOW_BOOKS, showBooksIntent);
actionMap.set(Actions.CONFIRM, confirmIntent)
actionMap.set(Actions.SELECTED_BOOK, selectedBookIntent)
app.handleRequest(actionMap);
})

module.exports = {
Functions
}

デプロイとAPI.AIとの紐付け

ここを参照するといい https://developers.google.com/actions/apiai/deploy-fulfillment

以下のことをやる

$ npm install -g firebase-tools
$ firebase login
$ firebase init
$ npm install
$ firebase deploy --only functions

deployが終わると console に

Function URL (Functions): https://hogehogehogehoge

みたいのが出てくるのでこれをコピーしておく

API.AIに戻り、 左端のFulfillmentをクリックしてDisabledになっている項目をEnabledにする そうするとwebhookを設定できるのでURLに先ほどの FUNCTION URL を入力する

Actionの登録

先ほどコード上でこのように定義したActionを実際にAPI.AIにも定義して、API.AIとコードを紐づける

const Actions = {
WELCOME: 'welcome',
SHOW_BOOKS: 'show.books',
CONFIRM: 'confirm',
SELECTED_BOOK: 'selected.book',
}

API.AI左端の intents の+ボタンを押して、intentを新規作成 その後はuser says, action, を設定する

user says には 「おすすめのコミックを教えて」 action には selected.book を記入した

スクリーンショット 2017-10-08 23.05.09.png

また、一番下に fulfillment -> use webhook というチェックボックスがあるのでチェックを入れる

これでIntentの定義完了

おなじようにSHOW_BOOKSのintentも作る さっきと同じように設定して行くのだが、唯一違うのはEventsにactions_intent_OPTIONを設定する点 これをするとリストなどの要素から選択されたときにそのインテントが呼ばれるようになる これが設定し終わったら完成

テスト

左の integrations -> google assistant を選択し、upload draft をする

スクリーンショット 2017-10-08 23.07.27.png

そうすると actions on google の console に飛ばされて、アプリの情報の入力を迫られる

とりあえず全部埋めておく

infomationの入力が終わったら左のタブのsimulatorという項目を選ぶ

あとは試す

こんなかんじになる 

https://photos.app.goo.gl/w4ckVZVEH00cXRaO2

Google Play とかと違ってデベロッパーアカウントを購入する必要はなさそう とりあえずできたぜ。リリースもしてみたぜ。

審査通るといいな

Actions-on-Google SDK を使う時にApiAiAppが読み込めない問題が起きた時の対処法

GoogleAssistant app を作るために node で actionsdkを読み込んだときに起きた問題

undefined:1
undefined
^

SyntaxError: Unexpected token u in JSON at position 0
at JSON.parse ()
at ApiAiApp.AssistantApp (/data/app/node_modules/actions-on-google/assistant-app.js:128:23)
at ApiAiApp (/data/app/node_modules/actions-on-google/api-ai-app.js:78:5)
at Server. (/data/app/app.js:16:13)
at Server. (/data/app/node_modules/engine.io/lib/server.js:472:22)
at Server. (/data/app/node_modules/socket.io/lib/index.js:307:16)
at emitTwo (events.js:125:13)
at Server.emit (events.js:213:7)
at parserOnIncoming (_http_server.js:602:12)
at HTTPParser.parserOnHeadersComplete (_http_common.js:116:23)

よくわからんけどこんな感じのエラーが起きる ライブラリの内部で使っているパーサー周りでなんか問題があるっぽい

github.com

調べて見たらbody-parserを使ったらうまくいったよって書いてあったのでその通りにしたらうまく行った

const bodyParser = require('body-parser');

const app = express();
app.use(bodyParser.json());

海外で買ったgooglehomeを日本語対応させる方法

遂に日本でもグーグルホームがリリースされましたね

 

そして、海外で買った奴らも日本で使えるようになりましたよ

 

やり方は簡単でグーグルホームアプリを更新して、グーグルホームの設定をやり直して、言語設定を日本語にするだけです

 

超簡単だけど、設定を一回やり直さなきゃなのを忘れてたので少し時間を無駄にしてしまいました

 

日本語対応は嬉しいけど、新しい起動コマンドの

「ねえ、グーグル」

はダサい気がするなぁ笑

ReactNativeのFlatListがsetStateをしてもリレンダーされない時の対処法

FlatListのItemの中でFlatListで定義したdata以外のstateの値を使ってコンポネントをレンダーしていると、setStateを使ってもリレンダーされないことがある

そんなときはFlatListのExtraDataにその値を渡してあげるといい

そうするとextraDataの値が変更された時もrerenderされるようになる

<FlatList
  data={this.state.tags}
  extraData={this.state.movie}
  keyExtractor={this.defaultKeyExtractor}
  renderItem={this.renderTag}
/>

それでも動かない時もたまにある そういう時はremoveClippedSubviews={false}を追加してあげる

ここを参考にしてやった github.com

ReactNativeでTypescriptを使うチュートリアル

個人でアプリを開発するとき、iosandroid の両方を書くのは嫌だなーと思いReactNativeを使うように心がけています。

 

基本的には満足しているのですが、型がないので色々辛いところがあってなんかいい方法無いかなーと探していたらTypescriptを見つけました。

Typescriptを使うのも初めてなので、いい感じのチュートリアルないかなと思って探していたらMicrosoftがだしてる良さげなrepositoryがあったのでそれを使うことにしました。

 

GitHub - Microsoft/TypeScript-React-Native-Starter: A starter template for TypeScript and React Native with a detailed README describing how to use the two together.

 

イマは取り敢えずこのチュートリアルにのっとって開発してます。

 

便利便利

Microsoftありがとう