scalaでjavaクラスを呼び出す

概要

仕事でScalaから別javaを呼び出すときに妙に悩んだのでメモ

以下の3パターンを列挙する

自分の状況

  • scalaよくわかってない
  • java全然しらない

javaからjavaを使う

javaの場合はclass名とファイル名が同じでないといけないらしい

以下のファイルを用意する

[ludwig125 JavaJava]$ cat SampleClass.java
public class SampleClass {
        public static String val = "hoge";

    public static int add(int x, int y) {
        return x + y;
    }
    public static int sub(int x, int y) {
        return x - y;
    }
}
[ludwig125 JavaJava]$
[ludwig125 JavaJava]$ cat useSample.java
public class useSample {
    public static void main(String[] args) {

        System.out.println(SampleClass.val);
        System.out.println(SampleClass.add(5, 3));
        System.out.println(SampleClass.sub(5, 3));
    }
}
[ludwig125 JavaJava]$

コンパイルして実行

[ludwig125 JavaJava]$ javac SampleClass.java useSample.java
[ludwig125 JavaJava]$ java useSample
hoge
8
2
[ludwig125 JavaJava]$

scalaからscalaを使う

以下のファイルを用意する

[ludwig125 ScalaScala]$ cat SampleClass.scala
class S {
  val v = "hoge"
  def add(a: Int, b: Int): Int = a + b
  def sub(a: Int, b: Int): Int = a - b
}
[ludwig125 ScalaScala]$
[ludwig125 ScalaScala]$ cat useSample.scala
object useS {
  def main(args: Array[String]): Unit = {
    val s = new S
    println(s.v)
    println(s.add(5, 3))
    println(s.sub(5, 3))
  }
}
[ludwig125 ScalaScala]$

コンパイルして実行

[ludwig125 ScalaScala]$ scalac SampleClass.scala useSample.scala
[ludwig125 ScalaScala]$ scala useS
hoge
8
2
[ludwig125 ScalaScala]$

特記事項

scalaの場合javaと違ってファイル名とクラス名を一致させる必要はない 「val」がscalaの予約後なのでvに変えたが、valのまま使う方法もあったはず

scalaの場合、scalacでファイル名を指定してコンパイルしたあと、scalaコマンドの実行対象はクラス名(この場合useS)であることに注意

scalaからjavaを使う

以下のファイルを用意する

[ludwig125 ScalaJava]$ cat SampleClass.java
public class SampleClass {
        public static String v = "hoge";

    public static int add(int x, int y) {
        return x + y;
    }
    public static int sub(int x, int y) {
        return x - y;
    }
}
[ludwig125 ScalaJava]$
[ludwig125 ScalaJava]$ cat useSample.scala
object useS {
  def main(args: Array[String]): Unit = {
    println(SampleClass.v)
    println(SampleClass.add(5, 3))
    println(SampleClass.sub(5, 3))
  }
}
[ludwig125 ScalaJava]$

コンパイルして実行

javascalaを個別にコンパイルしたけど、まとめてやる方法があるのかどうかは確認してない

[ludwig125 ScalaJava]$ javac SampleClass.java
[ludwig125 ScalaJava]$ scalac useSample.scala
[ludwig125 ScalaJava]$ scala useS
hoge
8
2
[ludwig125 ScalaJava]$

Ubuntu 14.04のPython2のデフォルトバージョンをPython 2.7.9にする

たまたまやる必要があったのでメモ

参考

https://www.ralphlepore.net/installing-python-279-on-ubuntu-1404-lts/

このとおりやればできる

アップデート前

[~] $python2 --version
Python 2.7.6
[~] $

アップデート

mkdir ~/software
cd ~/software
wget https://www.python.org/ftp/python/2.7.9/Python-2.7.9.tgz

# Now we are going to unpack the tar and install python
tar -xvf Python-2.7.9.tgz
cd Python-2.7.9
./configure
make
sudo make install

反映させるために再起動 以下をする前に大事なものは保存しておく

# Restart server to ensure everything is up to date.
sudo shutdown now -r

アップデート後

[~] $python2 --version
Python 2.7.9
[~] $

vim8のインストール(Ubuntu 14.04)

vim8のインストール方法

OSのバージョン:Ubuntu 14.04 ※ Ubuntu 16.04でも以下と同じ方法でできるっぽい

現在のvimの確認

現在のvimのバージョンは以下で確認できる

方法1.vim --version

自分が実行したときの出力結果

[~] $vim --version                                
VIM - Vi IMproved 7.4 (2013 Aug 10, compiled Dec 18 2015 21:31:31)
適用済パッチ: 1-882
Modified by pkg-vim-maintainers@lists.alioth.debian.org
Compiled by buildd@
以下省略

方法2.dpkg -l vim

自分が実行したときの出力結果

[~] $dpkg -l vim
要望=(U)不明/(I)インストール/(R)削除/(P)完全削除/(H)保持
| 状態=(N)無/(I)インストール済/(C)設定/(U)展開/(F)設定失敗/(H)半インストール/(W)トリガ待ち/(T)トリガ保留
|/ エラー?=(空欄)無/(R)要再インストール (状態,エラーの大文字=異常)
||/ 名前                バージョン     アーキテクチャ 説明
+++-===================-==============-==============-===========================================
ii  vim                 2:7.4.826-1+da amd64          Vi IMproved - enhanced vi editor
[~] $

最新のvimのインストール

以下の3つのコマンドをさえすればOK 参考:http://tipsonubuntu.com/2016/09/13/vim-8-0-released-install-ubuntu-16-04/

sudo add-apt-repository ppa:jonathonf/vim
sudo apt update
sudo apt install vim

上のコマンドの意味

add-apt-repository

UbuntuではPPA(Personal Package Archives)というUbuntu非公式のソフトウェアを管理しているアーカイブをPPAというらしい 参考:https://allabout.co.jp/gm/gc/438675/

sudo add-apt-repository ppa:jonathonf/vim コマンドによって、PPAのvimリポジトリをこのUbuntuに認識させる

sudo apt update

パッケージの一覧を更新 リポジトリ追加・削除時には必ず実行するべきコマンド

sudo apt install vim

これで最新のvimをインストールすることができる

vimのバージョン確認

[~] $vim --version                                

VIM - Vi IMproved 8.1 (2018 May 17, compiled May 23 2018 13:04:51)
適用済パッチ: 1-20
Modified by pkg-vim-maintainers@lists.alioth.debian.org
Compiled by pkg-vim-maintainers@lists.alioth.debian.org
以下省略
[~] $dpkg -l vim
要望=(U)不明/(I)インストール/(R)削除/(P)完全削除/(H)保持
| 状態=(N)無/(I)インストール済/(C)設定/(U)展開/(F)設定失敗/(H)半インストール/(W)トリガ待ち/(T)トリガ保留
|/ エラー?=(空欄)無/(R)要再インストール (状態,エラーの大文字=異常)
||/ 名前                バージョン     アーキテクチャ 説明
+++-===================-==============-==============-===========================================
ii  vim                 2:8.1.0020-0yo amd64          Vi IMproved - enhanced vi editor
[~] $

おわり

【GoogleHomeでメモ帳アプリを作る】6. GoogleHomeに呼びかけてSpreadSheetのメモを読み上げてもらう

概要

前回までで、heroku上にアプリを作成するところまでできた

今度は、それをGoogleHomeから呼び出すため(正確にはactions on googleから呼び出すため)に、Dialogflowの設定をする

資料全体の構成はここに記載: GoogleHomeに話しかけてメモを記録したりメモを読み上げてもらう

参考

参考 actions on googleの公式マニュアル(Dialogflowについて)

Dialogflowの公式マニュアル

その他の参考

Dialogflowの詳しい作り方は上や色々なサイトに書いてあるのでここでは自分のアプリのために必要な最低限のことだけを記載する

Dialogflowでアプリ作成

actions on googleとDialogflowを連携する

https://developers.google.com/actions/

ACTIONS CONSOLE ボタンを押す

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

Add/import project を選択

Dialogflowを選択

※Dialogflow以外もあるようだが、参考資料が多かったのでここではDialogflowを使う

Dialogflowの設定

Intents

公式のマニュアル - https://dialogflow.com/docs/intents

新規のIntentsを作成 - ここでは、「 getMymemo 」 という名前のIntentsを作成

dialogflow_create

User saysに「 メモを読んで 」という文言を追加する

dialogflow_intents_new

一旦これでSAVE

Fulfillment

WebhookのURLにherokuのURLを追加する

dialogflow_fulfillment

これで

ENABLEDにする

IntentsとFulfillmentを連携

先程のIntentsに戻って、Fulfillmentの連携を有効にする

ここまででDialogflow側の最低限の設定は終わり

DialogflowからWebアプリへのリクエストとレスポンス制約

DialogflowからWebアプリを呼び出すためには以下のフォーマットに従う必要がある

公式のドキュメント - https://dialogflow.com/docs/fulfillment

Webhook

Setting up a webhook allows you to pass information from a matched intent into a web service and get a result from it.

Webhookの仕様(制約)

Request

Your web service receives a POST request from Dialogflow. 

Response

The response from the service should have the following fields:

Name Type Description
speech String Response to the request.
displayText String Text displayed on the user device screen.
data Object Additional data required for performing the action on the client side. The data is sent to the client in the original form and is not processed by Dialogflow.
contextOut Array of context objects Array of context objects set after intent completion.
source String Data source.
followupEvent Object Event name and optional parameters sent from the web service to Dialogflow. Read more

Caution: The header must be “Content-type: application/json”.

つまり要約すると・・・ - リクエスト - リクエストはPOSTでJSON形式で送られる - レスポンス - アプリからのレスポンスはJSON形式で、ヘッダーは“Content-type: application/json” である必要がある - レスポンスにはspeech, displayTextなどの項目が必要らしい

heroku(Django)の修正

  前述の仕様を満たすように、Djangoアプリの修正をする

https://github.com/ludwig125/googlehome/blob/master/memorandum/views.py

必要な項目を返すようにする

  • HttpResponse になっていた部分を以下のように書き直す
  • また、レスポンスの項目として「speech」にSpreadSheetの最新の項目を返すようにする

差分

git diff views.py

diff --git a/memorandum/views.py b/memorandum/views.py
index a544014..65e37fc 100644
--- a/memorandum/views.py
+++ b/memorandum/views.py
@@ -1,4 +1,5 @@
-from django.http import HttpResponse
+from django.http import JsonResponse
 import json
 import gspread
 import oauth2client.client
@@ -32,4 +33,12 @@ def get_value(gc):
 def index(request):
     gc = get_client()
     last_value = get_value(gc)
-    return HttpResponse(last_value)
+    res = {
+        "speech": last_value[2],
+        "displayText": ””,
+        "data": "",
+        "contextOut": "",
+        "source": ""
+    }
+    return JsonResponse(res)
JsonResponseとは

参考Django公式 - https://docs.djangoproject.com/en/2.0/ref/request-response/#jsonresponse-objects

An HttpResponse subclass that helps to create a JSON-encoded response. It inherits most behavior from its superclass with a couple differences: Its default Content-Type header is set to application/json.

他の参考 - https://simpleisbetterthancomplex.com/tutorial/2016/07/27/how-to-return-json-encoded-response.html

Its default Content-Type header is set to application/json.

とあるので、これを使えばfullfilementの制約が満たせそう

ブラウザから呼び出して確認

これでDjangoサーバを立ててみる

(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome] $python manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).
February 11, 2018 - 22:00:40
Django version 2.0.2, using settings 'googlehome.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
[11/Feb/2018 22:00:44] "GET /memorandum/ HTTP/1.1" 200 109
[11/Feb/2018 22:01:06] "GET /memorandum/ HTTP/1.1" 200 79

http://127.0.0.1:8000/memorandum/

→ 「{"speech": "test3", "displayText": "", "contextOut", "", "source": ""}」

ただ、POSTリクエストで送って返ってくるかどうかがわからない

Advanced REST client でリクエストを送って確認

Advanced REST client - https://chrome.google.com/webstore/detail/advanced-rest-client/hgmloofddffdnphfgcellkdfbfbjeloo?hl=ja

これを使うと、GET, POSTリクエストを選んだり、レスポンスのステータスをチェックしたり色々便利、これで確認する

これでPOSTリクエストを送ると、結果が返ってきた!!

advancedrestclient_test1

これをDialogflowが要求する形式に直す

このあたりのサンプルを見ると、dataとcontextOutはコメントアウトしていても問題なさそうなので、コメントアウトする - https://qiita.com/ume1126/items/f60335179b1cee25fa2f - https://github.com/dialogflow/fulfillment-webhook-weather-python/blob/966959087a5c82a50b21ce43515c7133e092f13c/app.py#L108-L112

views.pyを以下のように書き換える

from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
import json
import gspread
import oauth2client.client
import os

def get_client():
    """
    return google spreadsheet client
    see, http://gspread.readthedocs.io/en/latest/#gspread.authorize
    """
    scope = ['https://spreadsheets.google.com/feeds']
    client_email = os.environ['CLIENT_EMAIL']
    private_key = os.environ['PRIVATE_KEY'].replace('\\n', '\n').encode('utf-8')
    credentials = oauth2client.client.SignedJwtAssertionCredentials(client_email, private_key, scope)
    gc = gspread.authorize(credentials)
    return gc


def get_value(gc):
    """
    get google spreadsheet values
    return last value
    """
    ss = gc.open("memorandum")
    sh = ss.worksheet("memo1")
    values = sh.get_all_values()
    
    last = len(values) - 1
    return values[last]


@csrf_exempt
def index(request):
    gc = get_client()
    last_value = get_value(gc)
    res = {
        "speech": last_value[2],
        "displayText": last_value[2],
        #"data": {"kik": {}},
        #"contextOut": [{"name":"weather", "lifespan":2, "parameters":{"city":"Rome"}}],
        "source": "spreadsheet memorandum"
    }
    return JsonResponse(res)

@csrf_exempt は以下を参考に付けた https://docs.djangoproject.com/ja/2.0/ref/csrf/

クロスサイトリクエストフォージェリ (CSRF) 対策

上の内容をherokuにデプロイして(git push heroku masterして) Advanced REST clientを使うと200が返ってきた

advancedrestclient_test2

修正したアプリに対してリクエストしてみる

Dialogflowからリクエストを送る

dialogflowの画面で、「try it now」に「メモを読んで」と入れてみると、test3が返ってきた

dialogflow_intents

SHOW JSONボタンを押すと以下が得られる

{
  "id": "XXXXXX",
  "timestamp": "2018-02-12T12:43:43.804Z",
  "lang": "ja",
  "result": {
    "source": "agent",
    "resolvedQuery": "メモを読んで",
    "action": "",
    "actionIncomplete": false,
    "parameters": {},
    "contexts": [],
    "metadata": {
      "intentId": "XXXXXX",
      "webhookUsed": "true",
      "webhookForSlotFillingUsed": "false",
      "webhookResponseTime": 1153,
      "intentName": "getMymemo"
    },
    "fulfillment": {
      "speech": "test3",
      "source": "spreadsheet memorandum",
      "displayText": "test3",
      "messages": [
        {
          "type": 0,
          "speech": "test3"
        }
      ]
    },
    "score": 1
  },
  "status": {
    "code": 200,
    "errorType": "success",
    "webhookTimedOut": false
  },
  "sessionId": "XXXXXX"
}

※IDは適当にXXXXXXとかに置き換えている

actions on googleでテストする

テストまでできたらintegrations - integrationsの方法は一番上に書いた参考ページなどを見ればすぐわかる

ここまでで、Actions on Googleで以下のようにテストすると、スプレッドシートからかえってくることが確認できた

自分「テスト用アプリにつないで」と記入

画面「はい、テスト用アプリのテストバージョンです。」 「こんにちは!」

自分「メモを読んで」

画面「test3」と表示された

simulator1

simulator2

GoogleHomeで実際に動作を確認する

これをGoogleHome相手にやっても返ってきた!!

自分「OK Google, テスト用アプリにつないで」とGoogleHomeに話す

GoogleHome「はい、テスト用アプリのテストバージョンです。」 「こんにちは!」

自分「メモを読んで」

GoogleHome「test3」

最新のデータを読み上げさせるところまで成功した!

Dialogflowから送ったリクエストに応じて処理を変える

  • Dialogflowから送ったリクエストのパラメータを見て、それに応じた件数だけSpreadsheetからデータを返すように修正する

Dialogflow参考

公式
- https://dialogflow.com/docs/fulfillment - https://qiita.com/kenz_firespeed/items/0979ceb05e4e3299f313

他 - https://qiita.com/sakamoto_koji/items/8a9bb8a3f2063b4b5f34

Dialogflowからリクエストを送る

Intentsを修正

試しにDialogflowからリクエストを送ってみる

「1件読んで」 というサンプルリクエストを書いて、PARAMETER NAME に「number」を、「1」の部分にENTITYの@sys.numberを割り当てる

これによって、「1件読んで」をリクエストすれば、numberに1が、「3件読んで」とすればnumberに3が入る事になる

試しにTry it nowに「3件読んで」を入れて送ってみる

dialogflow_request1

リクエストの中身を確認

今回は、herokuのview.py側で出力させるdisplayTextを以下のようにして、何が送られているか確認してみる

req = json.loads(request.body.decode('utf-8'))

res = {
    "speech": last_value[2],
    "displayText":  str(req),
    "source": "spreadsheet memorandum"
}
  • リクエストの中身はエンコードされているので、decode関数をつける必要があることに注意

SHOW JSONを押すと送った内容とレスポンスが見られる JSON

{
  "id": "XXXXXX",
  "timestamp": "2018-02-23T14:27:16.974Z",
  "lang": "ja",
  "result": {
    "source": "agent",
    "resolvedQuery": "1件読んで",
    "action": "",
    "actionIncomplete": false,
    "parameters": {
      "number": 3
    },
    "contexts": [],
    "metadata": {
      "intentId": "XXXXXX",
      "webhookUsed": "true",
      "webhookForSlotFillingUsed": "false",
      "webhookResponseTime": 983,
      "intentName": "getMymemo"
    },
    "fulfillment": {
      "speech": "test3",
      "source": "spreadsheet memorandum",
      "displayText": "{'id': 'XXXXXX', 'timestamp': '2018-02-23T14:27:16.974Z', 'lang': 'ja', 'result': {'source': 'agent', 'resolvedQuery': '1件読んで', 'speech': '', 'action': '', 'actionIncomplete': False, 'parameters': {'number': 1}, 'contexts': [], 'metadata': {'intentId': 'XXXXXX', 'webhookUsed': 'true', 'webhookForSlotFillingUsed': 'false', 'intentName': 'getMymemo'}, 'fulfillment': {'speech': '', 'messages': [{'type': 0, 'speech': ''}]}, 'score': 1.0}, 'status': {'code': 200, 'errorType': 'success', 'webhookTimedOut': False}, 'sessionId': 'XXXXXX'}",
      "messages": [
        {
          "type": 0,
          "speech": "test3"
        }
      ]
    },
    "score": 1
  },
  "status": {
    "code": 200,
    "errorType": "success",
    "webhookTimedOut": false
  },
  "sessionId": "XXXXXX"
}

リクエストの中身をそのまま返せていることが確認できた

リクエストの中身から必要な項目を取り出す

  • 「○件読んで」の○部分を取り出したい

リクエストで来ているJSONの中身は「request」の中の「parameters」の中の「number」で取得できる

よって、以下のようにしてやれば、「○件読んで」の○が取得できるはず

@csrf_exempt
def index(request):

    req = json.loads(request.body.decode('utf-8'))
    number = req['result']['parameters']['number']

    gc = get_client()
    last_value = get_value(gc)
    res = {
        "speech": last_value[2],
        "displayText":  number,                                                                                   
        "source": "spreadsheet memorandum"
    }
    return JsonResponse(res)

試しにDialogflowのSHOW JSONで得られたJSONをAdvanced REST client のクエリに入れて実行するとこうなる

{
speech: "test3",
displayText: 3,
source: "spreadsheet memorandum"
}

「3」が取得できている

あとはこのnumberを使って、それだけの件数Spreadsheetのデータを読み込んで返すように修正すればいい

dialogflow_request2

Entitiesを修正

現時点の課題

  1. 件数を直接指定する必要がある
  2. GoogleHome想定通りに命令を認識しれてくれ無いことがある

Intentsを上述のように追加したことで「3件読んで」「5件読んで」というように件数を数字を指定すればそれだけの件数返してもらえるようになった しかし、毎回「○件読んで」と件数を指定するのは面倒なときもある

また、GoogleHomeから呼び出すと、うまく「3件読んで」などと認識してもらえず、「三件読んで」や「三件呼んで」と認識されてしまうことがある - ※どう発声したと認識されているかはGoogleHomeアプリのアクティビティを見れば履歴がわかる

解決方法としてEntitiesに登録する

上の課題を解決するためにEntitiesを使う

Entities - https://dialogflow.com/docs/entities

Entities are powerful tools used for extracting parameter values from natural language inputs. Any important data you want to get from a user's request, will have a corresponding entity

Entitiesでは、上にあげたような、「3件読んで」「三件読んで」「三件呼んで」などの違った命令が来たときに、等しく「3」をリクエストパラメータとするように扱ってもらうことができる

DialogflowのEntitiesを開いて、「request_nums」として以下のように登録する

左側がリクエストとして送られる数字 シノニム                                            
1                             1, メモを読んで, 1件読んで, 一件読んで, いっけんよんで  |  
2                             2, 2件読んで, 二件読んで, 二件呼んで, にけんよんで
3                             3, 3件読んで, 三件読んで, 三件呼んで, さんけんよんで    
3                             3, いくつか読んで, いくつか呼んで, いくつか, いくつかよんで  

こんな感じに色々なバリエーションを設定しておく

dialogflow_entity

アプリの修正

前述のnumber以外に、request_numsがあったときはこれを件数として扱ってそれだけデータを返すようにアプリを修正する

修正後 - https://github.com/ludwig125/googlehome/blob/master/memorandum/views.py

これで、件数を指定しても、「いくつか」などと言っても柔軟にデータを読み上げてもらえるようになった

【GoogleHomeでメモ帳アプリを作る】7. 使い勝手を良くする

概要

いままで作ったアプリの使い勝手を良くする

資料全体の構成はここに記載: GoogleHomeに話しかけてメモを記録したりメモを読み上げてもらう

やること

やることは以下

  1. Herokuをスリープさせないようにする
  2. アプリの名前を変える
  3. ショートカットを作って呼び出しを簡単にする
  4. Herokuのアプリの名前を変更する

1. Herokuをスリープさせないようにする

現時点の課題:herokuがスリープしてしまってタイムアウトする

GoogleHomeからactions on googleのfullfilmentから実行するためには、5秒以内に返さなくてはいけないらしい

参考公式 - https://developers.google.com/actions/sdk/deploy-fulfillment

Note: Your fulfillment must respond within 5 seconds or the Assistant ends your conversation due to a timeout.

https://dialogflow.com/docs/fulfillment#limits

Timeout for service response – 5 seconds.

  • herokuは無料版だと30分以上操作がないときはスリープしてしまう
  • スリープを解除して応答するには時間がかかり、5秒のタイムアウトを超えてしまうことがしばしばある
  • これでは使い物にならないので、herokuをスリープさせないようにする必要がある

参考 herokuの無料枠で使える時間 - https://devcenter.heroku.com/articles/free-dyno-hours#usage

Accounts are given a base of 550 hours each month in which your Free dynos can run. In addition to these base hours, accounts which verify with a credit cardwill receive an additional 450 hours to the monthly Free dyno quota.

http://awesome-linus.tk/2017/12/14/heroku-pricing/

クレジットカード認証なしだと、月に550時間、クレジットカード認証ありだと月に1000時間(550+450)使える

スケジューラを設定して定期的に叩く

公式

以下を参考に設定

こんなのもあった

[~/git/ludwig125-heroku/googlehome] $heroku addons:create scheduler:standard         (git)-[master]
 ▸    heroku-cli: update available from 6.14.43-73d5876 to 6.15.26-5726b6f
Creating scheduler:standard on ⬢ aqueous-peak-42683... free
This add-on consumes dyno hours, which could impact your monthly bill. To learn more:
http://devcenter.heroku.com/addons_with_dyno_hour_usage

To manage scheduled jobs run:
heroku addons:open scheduler

Created scheduler-horizontal-89891
Use heroku addons:docs scheduler to view documentation
[~/git/ludwig125-heroku/googlehome] $   

UIで以下のコマンドを10分おきに実行するようにする

if [ $(date --date "9hours" +%H) -ge 06 -o $(date --date "9hours" +%H) -le 02 ]; then curl https://aqueous-peak-42683.herokuapp.com ; fi
コマンドの説明
  • curlでアプリのURLを指定して実行する
  • 流石に午前2時から6時の間は使うことも無いはずなので、それ以外の時間に叩くようにしたい
  • herokuの時間はGMT標準時なので、日本時間より9時間前
  • そこで、「if [ $(date --date "9hours" +%H) -ge 06 -o $(date --date "9hours" +%H) -le 02 ]」とする
指定の時刻に実行できているか確認

heroku logs の結果を見る

2018-03-01T14:54:57.067107+00:00 app[scheduler.6122]: </body>
2018-03-01T14:54:57.067108+00:00 app[scheduler.6122]: </html>
2018-03-01T15:05:47.980054+00:00 app[api]: Starting process with command `if [ $(date --date "9hours" +%H) -ge 06 -o $(date --date "9hours" +%H) -le 02 ]; then curl https://aqueous-peak-42683.herokuapp.com ; fi` by user scheduler@addons.heroku.com
2018-03-01T15:05:54.034812+00:00 heroku[scheduler.5767]: Starting process with command `if [ $(date --date "9hours" +%H) -ge 06 -o $(date --date "9hours" +%H) -le 02 ]; then curl https://aqueous-peak-42683.herokuapp.com ; fi`
2018-03-01T15:05:54.827763+00:00 heroku[scheduler.5767]: State changed from starting to up
2018-03-01T15:05:56.537126+00:00 heroku[router]: at=info method=GET path="/" host=aqueous-peak-42683.herokuapp.com request_id=86071458-7414-42f7-b246-e056260137f6 fwd="174.129.137.75" dyno=web.1 connect=1ms service=7ms status=404 bytes=2344 protocol=https
2018-03-01T15:05:56.536624+00:00 app[web.1]: Not Found: /
2018-03-01T15:05:56.537059+00:00 app[web.1]: 10.151.49.221 - - [02/Mar/2018:00:05:56 +0900] "GET / HTTP/1.1" 404 2161 "-" "curl/7.47.0"
2018-03-01T15:05:56.265126+00:00 app[scheduler.5767]:                                  Dload  Upload   Total   Spent    Left  Speed
2018-03-01T15:05:56.265039+00:00 app[scheduler.5767]:   % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
2018-03-01T15:05:56.699372+00:00 heroku[scheduler.5767]: State changed from up to complete
(略)

エラーは出しているものの、叩けているのでこれでOK

アプリが起動しているかスリープしているかはアプリのアイコンを見ればわかる - https://blog.heroku.com/app_sleeping_on_heroku

2. アプリの名前を変える

actions on googleから変更する アプリとして登録するために、アプリ用のアイコンの画像が必要だったりするので結構めんどい そして、アプリ用の画像の判定が厳しすぎて辛い・・・

アプリの名前は簡単に「メモ帳」とした

これで、以下の言い方でつながる様になった

呼び出し方1

「メモ帳につないで」

「はい、メモ帳のテストバージョンです。こんにちは」

「メモを読んで」

直近のメモが読まれる

呼び出し方2

上の方法だと冗長なので、以下のような言い方で一度に実行することもできる

「メモ帳を使ってメモを読んで」

「はい、メモ帳のテストバーンジョンです。こんにちは」

直近のメモが読まれる

3. ショートカットを作って呼び出しを簡単にする

GoogleHomeでショートカットを作成することで、「メモ帳を使ってメモを読んで」をもっと簡略化した言い方で済むようにする

ショートカットの言葉 - 「メモを読んで」 - 「メモ読んで」

実際に実行される命令 - 「メモ帳を使ってメモを読んで」

4. Herokuのアプリの名前を変更する

  • ずっとherokuが自動で発行したアプリ名「aqueous-peak-42683 」を使っていたので、これをわかりやすい名前に変える

変え方参考 - https://devcenter.heroku.com/articles/renaming-apps

$heroku apps:rename 新しいアプリ名

合わせて、dialogflowのFulfillmentで呼び出すURLと、HerokuのスケジューラでcurlするURLを変える必要があるので注意

【GoogleHomeでメモ帳アプリを作る】5. HerokuアプリでSpreadSheetの中身を取得する

概要

以前helloアプリは作ったので、ここではSpreadSheetからデータを取得するアプリを作る

この前まででSpreadSheetは用意できているので、 ここでは、Spreadsheetからデータを取得するアプリケーションを「memorandum」という名前でDjangoに追加する

資料全体の構成はここに記載: GoogleHomeに話しかけてメモを記録したりメモを読み上げてもらう

memorandumアプリ

アプリ作成のおさらい

まずはhelloのおさらいを兼ねて、動くアプリを作成してみる

アプリの作り方は公式のチュートリアルの通り、 https://docs.djangoproject.com/ja/2.0/intro/tutorial01/#creating-the-polls-app

python manage.py startapp memorandum でいい

事前にpipenv shellを起動しておく

[~/git/ludwig125-heroku/googlehome] $pipenv shell                                                                          (git)-[master]
Loading .env environment variables…
Spawning environment shell (/usr/bin/zsh). Use 'exit' to leave.
source /home/ludwig125/.local/share/virtualenvs/googlehome-Z81pgPAi/bin/activate
[~/git/ludwig125-heroku/googlehome] $source /home/ludwig125/.local/share/virtualenvs/googlehome-Z81pgPAi/bin/activate      (git)-[master]
(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome] $  

アプリ作成

(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome] $python manage.py startapp memorandum                            (git)-[master]
(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome] $ls -l                                                           (git)-[master]
合計 84
-rw-rw-r-- 1 ludwig125 ludwig125   183  2月  4 02:07 Pipfile
-rw-rw-r-- 1 ludwig125 ludwig125  7710  2月  4 22:54 Pipfile.lock
-rw-rw-r-- 1 ludwig125 ludwig125    43  2月  4 03:00 Procfile
-rw-r--r-- 1 ludwig125 ludwig125 38912  2月  4 00:50 db.sqlite3
drwxrwxr-x 3 ludwig125 ludwig125  4096  2月  4 23:39 googlehome/
drwxrwxr-x 4 ludwig125 ludwig125  4096  2月  4 02:38 hello/
-rwxrwxr-x 1 ludwig125 ludwig125   542  2月  3 23:57 manage.py*
drwxrwxr-x 3 ludwig125 ludwig125  4096  2月  7 23:55 memorandum/
-rw-rw-r-- 1 ludwig125 ludwig125   269  2月  4 23:05 requirements.txt
drwxrwxr-x 3 ludwig125 ludwig125  4096  2月  4 02:11 staticfiles/
(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome] $  
(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome] $tree memorandum                                                 (git)-[master]
memorandum
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│   └── __init__.py
├── models.py
├── tests.py
└── views.py

1 directory, 7 files
(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome] $    

views.py

おさらいで、とりあえず文字を返すアプリを作って動作を確認する

(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome] $cat memorandum/views.py (git)-[master]

from django.http import HttpResponse


def index(request):
    return HttpResponse("Hello, world. You're at the memorandum index.")

urls.py

helloのときと同様に以下のファイルを追加

(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome] $cat  memorandum/urls.py                                         (git)-[master]
from django.urls import path

from . import views

urlpatterns = [
    path('', views.index, name='index'),
]
(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome] $ 

ここも同様に、プロジェクトのurlsに上のurlsを認識させる

(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome] $cat  googlehome/urls.py                                         (git)-[master]

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('hello/', include('hello.urls')),
    path('memorandum/', include('memorandum.urls')),  ← ここを追加
    path('admin/', admin.site.urls),
]
(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome] $ 

ローカルでアプリ立ち上げ

ここまででアプリを立ち上げる

http://127.0.0.1:8000/memorandum/ ブラウザで以下が返ってきた

→ 「Hello, world. You're at the memorandum index.」

herokuに反映

ここまでをherokuにも反映する

(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome] $git add memorandum/                                             (git)-[master]
(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome] $git commit -m 'add initial memorandum' .                        (git)-[master]
[master 9122dd7] add initial memorandum
 9 files changed, 27 insertions(+)
 create mode 100644 memorandum/__init__.py
 create mode 100644 memorandum/admin.py
 create mode 100644 memorandum/apps.py
 create mode 100644 memorandum/migrations/__init__.py
 create mode 100644 memorandum/models.py
 create mode 100644 memorandum/tests.py
 create mode 100644 memorandum/urls.py
 create mode 100644 memorandum/views.py
(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome] $   
(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome] $git push heroku master                                          (git)-[master]
Counting objects: 15, done.
Compressing objects: 100% (11/11), done.
Writing objects: 100% (11/11), 1.36 KiB | 0 bytes/s, done.
Total 11 (delta 5), reused 0 (delta 0)
remote: Compressing source files... done.

リモートのherokuを確認

$heroku open  

https://aqueous-peak-42683.herokuapp.com/memorandum/「Hello, world. You're at the memorandum index.」

おさらい終わり

今作ったアプリをもとに、動作を確認しつつ修正していく

GoogleAPI秘密鍵の取得とSpreadSheetの共有設定

以下を参考に、GoogleAPIを取得し、SpreadSheetをAPIで呼べるようにする

参考 - https://qiita.com/koyopro/items/d8d56f69f863f07e9378 - https://qiita.com/AAkira/items/22719cbbd41b26dbd0d1 - http://www.yoheim.net/blog.php?q=20160205

GoogleAPIのクライアント認証用JSONを取得

cloud-resource-manager - https://console.developers.google.com/cloud-resource-manager ここで適当なプロジェクトを作成

ここでは、「getMymemo」というプロジェクトを作成する

上の参考を見ながら、作っていく

https://console.developers.google.com/cloud-resource-manager から 「APIとサービス」→ 「APIとサービスの有効化」からGoogle Drive APIを選択

googleapis1 googleapis2 googleapis3

→「認証情報」→「認証情報を作成」→「サービスアカウントキー」

googleapis4 googleapis5 googleapis6

秘密鍵JSONファイルを取得できたらOK

SpreadSheetの共有設定

「memorandum」シートに上記で発行されたメールアドレスを登録

googleapis7

pythonでGoogleSpreadSheetにアクセスできるか確認

JSONファイルを直接使う

client認証用のjsonを以下に置く

(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome] $cat google_client.json                                     (git)-[master] 
{   
  "type": "service_account",    
  "project_id": "ludwig125-37f7c",  
  "private_key_id": XXXXXX  
  "private_key": XXXXXX 
  "client_email": XXXXXX    
  "client_id": "XXXXXX",    
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",  
  "token_uri": "https://accounts.google.com/o/oauth2/token",    
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",  
  "client_x509_cert_url": "XXXXXX"  
}   
(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome] $     
パッケージ追加

pythonでSpreadSheetにアクセスするために以下のパッケージを入れておく

pip install gspread
pip install "oauth2client<2.0"
pip install pycrypto

pythonでSpreadSheetのデータを取得する

pythonでSpreadsheetのデータが取れるか確認する

gspreadのリファレンスは以下 - http://gspread.readthedocs.io/en/latest/

サンプルスクリプト作成

このときのspreadsheetは以下のデータが存在している状態

                                     
テスト
テスト に
1517843323 2018/2/6 0:08 テスト さん
1517843115 2018/2/6 0:05 test
1517844297 2018/2/6 0:24 test2
1518354065 2018/2/11 22:01 test3

(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome/sample] $cat get_memo.py

import json
import gspread
import oauth2client.client

json_key = json.load(open('../google_client.json'))
scope = ['https://spreadsheets.google.com/feeds']
credentials = oauth2client.client.SignedJwtAssertionCredentials(json_key['client_email'], json_key['private_key'].encode(), scope)
gc = gspread.authorize(credentials)

ss = gc.open("memorandum")
sh = ss.worksheet("memo1")

values = sh.get_all_values()
print(values)    # [['', '', 'テスト'], ['', '', 'テスト に'], ['1517843323', '2018/02/06 0:08:43', 'テスト さん'], ['1517843115', '2018/02/06 0:05:15', 'test'], ['1517844297', '2018/02/06 0:24:57', 'test2'], ['1518017810', '2018/02/08 0:36:50', 'test3']]

print(len(values))   # 6    
last = len(values) - 1
print(values[last])  # ['1517844303', '2018/02/06 0:25:03', 'test3']

実行結果

(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome/sample] $python3 get_memo.py                                      (git)-[master]
[['', '', 'テスト'], ['', '', 'テスト に'], ['1517843323', '2018/02/06 0:08:43', 'テスト さん'], ['1517843115', '2018/02/06 0:05:15', 'test'], ['1517844297', '2018/02/06 0:24:57', 'test2'], ['1518017810', '2018/02/08 0:36:50', 'test3']]
6
['1518017810', '2018/02/08 0:36:50', 'test3']
(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome/sample] $

きちんと取れている

秘密鍵をファイルではなく環境変数から取るようにする

上のソースコードの方法をherokuでやろうとすると、gitにGoogleAPIの秘密鍵を置かないといけない

これは嫌だ

http://hakobera.hatenablog.com/entry/20111122/1321975203 の辺を見ると、herokuには環境変数を渡すことができそうなので、この方法をやってみる

jsonファイルの内容を環境変数として以下のように設定しておく

export CLIENT_EMAIL="<自分のemail>.com"
export PRIVATE_KEY="-----BEGIN PRIVATE KEY-----<改行コード含む長い鍵>\n-----END PRIVATE KEY-----\n"

秘密鍵の情報をファイルから取っていたこの部分を

$ cat get_memo.py
json_key = json.load(open('../google_client.json'))
scope = ['https://spreadsheets.google.com/feeds']
credentials = oauth2client.client.SignedJwtAssertionCredentials(json_key['client_email'], json_key['private_key'].encode(), scope)

$cat get_memo_with_env.py   
scope = ['https://spreadsheets.google.com/feeds']
client_email = os.environ['CLIENT_EMAIL']
private_key = os.environ['PRIVATE_KEY'].replace('\\n', '\n').encode('utf-8')
credentials = oauth2client.client.SignedJwtAssertionCredentials(client_email, private_key, scope) 

こんな感じに変えて、取得できることを確認する

※ private_keyは改行コードを含んでおり、これをenvironで取得すると「\」が「\\」になってしまったので、置換して「\n」にした 参考 - https://docs.python.jp/3/library/os.html - https://webkaru.net/linux/export-command/ - https://docs.python.jp/3/howto/unicode.html#converting-to-bytes

実行してスプレッドシートから取得できることを確認

(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome/sample] $python3 get_memo_with_env.py                           (git)-[master]
[['', '', 'テスト'], ['', '', 'テスト に'], ['1517843323', '2018/02/06 0:08:43', 'テスト さん'], ['1517843115', '2018/02/06 0:05:15', 'test'], ['1517844297', '2018/02/06 0:24:57', 'test2'], ['1518017810', '2018/02/08 0:36:50', 'test3']]
6
['1518017810', '2018/02/08 0:36:50', 'test3']
(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome/sample] $  

ローカルのherokuで実行する

以上のことをもとに、view.pyを修正する

(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome] $cat memorandum/views.py

from django.http import HttpResponse
import json
import gspread
import oauth2client.client
import os

def get_client():
    """
    return google spreadsheet client
    see, http://gspread.readthedocs.io/en/latest/#gspread.authorize
    """
    scope = ['https://spreadsheets.google.com/feeds']
    client_email = os.environ['CLIENT_EMAIL']
    private_key = os.environ['PRIVATE_KEY'].replace('\\n', '\n').encode('utf-8')
    credentials = oauth2client.client.SignedJwtAssertionCredentials(client_email, private_key, scope) 
    gc = gspread.authorize(credentials)
    return gc

def get_value(gc):
    """
    get google spreadsheet values
    return last value
    """
    ss = gc.open("memorandum")
    sh = ss.worksheet("memo1")
    values = sh.get_all_values()
    
    last = len(values) - 1
    return values[last]


def index(request):
    gc = get_client()
    last_value = get_value(gc)
    return HttpResponse(last_value) 

ローカルで実行

(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome] $heroku local web                                              (git)-[master]
 ▸    heroku-cli: update available from 6.14.43-73d5876 to 6.15.24-e5de04c
[OKAY] Loaded ENV .env File as KEY=VALUE Format
23:40:49 web.1   |  [2018-02-09 23:40:49 +0900] [18423] [INFO] Starting gunicorn 19.7.1
23:40:49 web.1   |  [2018-02-09 23:40:49 +0900] [18423] [INFO] Listening at: http://0.0.0.0:5000 (18423)
23:40:49 web.1   |  [2018-02-09 23:40:49 +0900] [18423] [INFO] Using worker: sync
23:40:49 web.1   |  [2018-02-09 23:40:49 +0900] [18426] [INFO] Booting worker with pid: 18426

http://localhost:5000/memorandum/

→ 「'1518017810', '2018/02/08 0:36:50', 'test3'」

取得できた

リモートのherokuで取れるようにする

requirements.txt編集

リモートで使うために必要なパッケージをrequirements.txtに入れておく - ローカルで使ったときにインストールした以下のパッケージを追加する - 何のバージョンを入れたのかは、pip freezeで確認

(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome] $pip freeze | egrep "gspread|oauth2client|pycrypto"              (git)-[master]
gspread==0.6.2
oauth2client==1.5.2
pycrypto==2.6.1
(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome] $  

requirements.txt - 既存のパッケージ以外に以下を追加する

(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome] $cat requirements.txt                                            (git)-[master]
(略)
gspread==0.6.2
(略)
oauth2client<2.0
(略)
pycrypto==2.6.1
(略)
(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome] $ 

herokuに環境変数を追加

以下を見るとherokuのリモートに設定値を渡すことができるらしいので、 - https://devcenter.heroku.com/articles/config-vars#example - https://qiita.com/colorrabbit/items/18db3c97734f32ebdfde - http://hakobera.hatenablog.com/entry/20111122/1321975203

上の修正までをコミットしたらherokuに上げる

$ git push heroku master

ここで、heroku 環境変数を設定する

$ heroku config:set CLIENT_EMAIL="XXXX.com"
$ heroku config:set PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nXXXXXX-----END PRIVATE KEY-----\n"
herokuの環境変数の設定方法について
  1. 上のようにheroku config:set をするか、
  2. ダッシュボードから行う方法がある

参考 - https://devcenter.heroku.com/articles/config-vars#setting-up-config-vars-for-a-deployed-application

設定できたかどうかは以下のコマンドで確認できる

$heroku config  

リモートのherokuで確認

https://aqueous-peak-42683.herokuapp.com/memorandum/

→ 「1518017810 2018/02/08 0:36:50 test3」 Spreadsheetの最後のデータがブラウザに表示された

【GoogleHomeでメモ帳アプリを作る】4. IFTTTを使ってSpreadSheetにメモを入力

概要

前回までで色々と必要な知識を得て、準備が整ったのでこれから実際に必要な機能を作っていく

  • Spreadsheetに記録する部分の作成
  • Ok Google ~でSpreadSheetにメモを記録する
  • これはIFTTTを使うと便利
  • ここで記録したSpreadSheetの内容を次回以降のherokuアプリから呼び出す

資料全体の構成はここに記載: GoogleHomeに話しかけてメモを記録したりメモを読み上げてもらう

IFTTTとGoogleSpreadsheet

IFTTT

アプリの作成

https://ifttt.com/discover - New Appletを選択

ifttt_newapplet

if thisの部分

あとから編集した画面なので、初期作成時とは背景の色が違うが、記入内容は同じ

ifttt_ifthis

then thisの部分

ifttt_then

IFTTT注意

CreatedAtが効かないという問題があるらしい

IFTTTを試す

GoogleSpreadSheet

IFTTTによって作成されたシートを確認

IFTTTをつくったあとは スプレッドシートが以下のように作られる

spreadsheet1

GASを追加

上のGoogleHomeに喋って作成したテキストの左の2列に日付を自動で挿入されるようにしたい

「ツール」→「スクリプト エディタ」からGAS(Google Apps Script)を作成する

参考 https://so-zou.jp/web-app/tech/programming/javascript/grammar/object/date.htm

spreadsheet2

一応プログラムで取得しやすいことを考えてUnixTimeも出すようにしておいた

function addDate() {
  var sheet = SpreadsheetApp.getActiveSheet();
  var lastRow = sheet.getLastRow();
  var date = new Date();
  var unixTimestamp = Math.round( date.getTime() / 1000 );
  sheet.getRange(lastRow, 1).setValue(unixTimestamp);
  sheet.getRange(lastRow, 2).setValue(date);
}

spreadsheet3

プロジェクト名を決める

spreadsheet4

時計の形をしたアイコンをクリックして、 プロジェクトのトリガーとして - 実行:addDate
- イベント:スプレッドシートから 値の変更

spreadsheet5

ためしにデータを追加してみる

spreadsheet6

「test」「test2」を記入すると、GASによって、隣に勝手に時刻が記載された