Scala.jsでGoogle Apps Scriptを書いてみよう
はじめに
あのプログラミング言語だったらこんなことするのラクなのに〜 を Scala でやってみる第三弾です。
Scala やっててこんなこと思ったりしませんか?
- あー、Google Apps Script も Scala で書きたい
- TypeScript のライブラリを Scala からも活用したい
それいい感じにできるソリューションあります!
完成品はコチラ ignission/scalajs-google-apps-script-facade
その他シリーズの記事はコチラ:
- 第一弾: Scala で GraalVM native-image を作って AWS Lambda で動かす
- 第二弾: Scala.js でテトリスを作って AWS Amplify でホスティングしてみる
要約
- Scala.js を使えば Google Apps Script だって書けちゃう
- scala-js-ts-importer を使うことで、TypeScript の型定義を基に Scala.js から使えるようになる(変換後のものを facade と呼ぶらしい)
- TypeScript の型定義がなくても手作業で作れる
説明
今回はせっかくなので、facade の作り方と実際の使い方について分けてみようと思います。
共通編
Scala.js を使用するので、以下のように sbt plugin を追加します。
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.5.1")
plugin を有効化します。
enablePlugins(ScalaJSPlugin)
facade を作る
まず、以下のものを用意します。
- scala-js-ts-importer を適当な場所に git clone します
- 型定義ファイル(今回はここからDefinitelyTyped/types/google-apps-script) をダウンロードします
あとは scala-js-ts-importer の README に従って、コマンドを実行するだけで OK です。
sbt 'run some-lib.d.ts SomeLib.scala'
出来上がったSomeLib.scala
を、src/main/scala
ディレクトリ以下に配置します。
私の場合、d.ts
ファイルが多かったので 雑なスクリプト を書きました(シェルスクリプト弱者なので...)
あとは生成済みの Scala コードをライブラリ化して maven 等に publish すれば、通常のライブラリ同様扱うことができます。
例:
libraryDependencies += "tech.ignission" %%% "google-apps-script-scalajs-facade" % "0.5.0"
package の命名に悩んだのですが、aws-sdk-scalajs-facade を参考に、facade.googleappsscript
から始まるようにしました。
facade を利用して簡単な bot を作ってみる
今回は Typetalk に投稿すると、その内容を Google Sheets に追記していき、リストを返す bot を作ってみます。
Main のソースコードはコチラ
clasp のインストール
clasp は Google Apps Script をコマンドラインから操作できるライブラリです。
npm install -g @google/clasp
Google Sheets の作成
clasp のインストールが終わったら、実際にスクリプトを実行するために Google Sheets を作成します。
--type
にsheets
を指定していますが、Google Sheets が必要なかったらwebapp
としても OK です。
clasp create --type sheets --rootDir ./dist
appsscript.json
appsscript.json
はスクリプトのマニフェストのことで、様々な設定を記述できます。公式ドキュメントはコチラ
oauthScopes
はスクリプトに必要な承認スコープを定義します。外部との通信をする場合は https://www.googleapis.com/auth/script.external_request
、Google Sheets を使用する場合は https://www.googleapis.com/auth/spreadsheets
が必要です。
{
"timeZone": "America/New_York",
"dependencies": {
},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"webapp": {
"access": "ANYONE_ANONYMOUS",
"executeAs": "USER_DEPLOYING"
},
"oauthScopes": [
"https://www.googleapis.com/auth/script.external_request",
"https://www.googleapis.com/auth/spreadsheets"
]
}
試しにデプロイしてみる
一旦デプロイできるかチェックしましょう。
最小限の Scala コードを書きます。@JSExportTopLevel
でdoPost
を指定しているので、デプロイ後にスクリプトに対して発行された Post リクエストを処理できるようになります。
import facade.googleappsscript.DoPost
import facade.googleappsscript.GoogleAppsScript.ContentService
import facade.googleappsscript.content.{MimeType, TextOutput}
object Main {
@JSExportTopLevel("doPost")
def doPost(e: DoPost): TextOutput = {
ContentService
.createTextOutput(e.postData.contents)
.setMimeType(MimeType.JSON);
}
}
デプロイコマンドを実行します。
sbt example/deploy
ちなみにデプロイ周りを省力化できるように、sbt task をちょっと工夫してます。sbt に慣れてないのでツッコミどころ多いけどいろいろ改善できそうです。
通常であれば、clasp で push して Script Editor 上でデプロイをポチポチしないといけなくてすごく手間に感じました。
def deploy(cwd: String): Unit = {
val logger = new Logger
val dir = new File(cwd)
Process("clasp deployments", dir) ! logger.log
logger.print()
if (logger.out.length <= 2) {
logger.flush()
Process("clasp deploy", dir) ! logger.log
logger.print()
}
val deploymentRow = logger.out(logger.out.length - 1)
val deployId = deploymentRow.split(' ')(1)
logger.flush()
val deployCmd = s"clasp deploy -i $deployId"
Process(
Seq("clasp", "deploy", "-i", deployId, "-d", "deployed from sbt"),
dir
) ! logger.log
logger.print()
}
この sbt task では、deployment 一覧を取得してなければ作成、そして最後の deployment に対して再度デプロイというステップを自動でやってます。手前味噌ですがすごく便利です。
スクリプトに権限を与える(初回のみ)
デプロイして発行された URL にアクセスしたらエラーになってしまってハマったんですけど、最初の 1 回だけスクリプトに権限を与える必要があります。
力技で承認画面を表示させるために、Script Editor を開いて以下の関数を記述します。
function test() {
return ""
}
スクリプト追加例
次にtest
メソッドを選択して、Run
をクリックします。
Review permissions
をクリックして、問題なさそうだったら承認します。
モデルの定義
Typetalk から Outgoing Webhook がスクリプトに飛んでくるのですが、JSON 形式のため Scala で扱えるようにモデルを定義します。
@js.native
trait PostData extends js.Object {
val post: Post = js.native
val topic: Topic = js.native
}
@js.native
trait Post extends js.Object {
val id: js.Any = js.native
val message: String = js.native
val account: Account = js.native
}
@js.native
trait Topic extends js.Object {
val id: js.Any = js.native
val name: String = js.native
}
@js.native
trait Account extends js.Object {
val id: js.Any = js.native
val name: String = js.native
}
これらのモデルがあれば、以下のようにして JSON を parse できます。
val postData = JSON.parse(e.postData.contents).asInstanceOf[PostData]
次にレスポンスデータの定義をします。こちらは Scala からインスタンス生成を行う必要があるので、@js.native
アノテーションがついていないことにご注意ください。
trait TypetalkReply extends js.Object {
val message: String
val replyTo: js.Any
}
このようにしてインスタンス生成できます。
val reply = new TypetalkReply {
val message: String = contents.mkString("\n")
val replyTo: js.Any = data.post.id
}
JSON に変換できます。
val jsonString = JSON.stringify(reply)
動かしてみる
Script Editor からwebapp
の URL を取得して、Typetalk Bot を作ってみましょう。
Bot にメンションすると、その内容を Google Sheets に記録して、登録済みのリストを返します。
まったく使いどころがありませんね!
寿司食べたい!
おわりに
これで Google Apps Script だって Scala で書けるので安心ですね!
コード書いてコンパイルしてデプロイまで一貫して行えるので、ストレスなく開発できるのではないでしょうか。
どうしてもjs.Any
として表現せざるを得ない場面もありxxx.asInstanceOf[A]
といったキャストをしないといけないのが気になりますが、まぁ仕方ないかと割り切っています(そうですよね)。
Bot を作ってみてもいいし、Google Sheets 以外とも連携させてもいいし、是非活用してみてください。
Scala って面白そうだなって思ってもらえると嬉しいです!
Discussion