なにこれ
この記事はギフティ Advent Calendar 2019 - Qiita 4日目の記事です。
既存システムの問題点をなんかいい感じに取得して投げてくれるbotを作ったのでまとめます。
背景
現在のチームでは既存システムについて営業サイドなどから改善点・問題点としてGoogleスプレッドシートに既定のフォーマットで記載されていきます。
(この運用がそもそも良いものかは別の話として)ざっくばらんに挙がってくる課題のどれが優先的に対応する必要があるのか、どれが簡単に対応できるのかがわからないと対応するのが大変です。
そこで、セルに重要度や工数(作業の大変さ等)から適当に重みをつけた旬の課題を出したくなりました。
・・・で、スプレッドシートを操作することができる Google Apps Script
というものを利用するわけですが、そこの標準ファイル形式である.gs
ファイル(jsの独自改造?)をブラウザ上で取り扱うには幾らか問題がありました。
- そも論、すごく大変だった。classすらないし
- 手で操作できてしまう。誤削除怖すぎ。
- え、コミット履歴は?
ということで、もっと効果的に作りたいと思い至り今回はclaspを利用して開発をしていこうと思います。
事前準備
claspをインストール
# npmの場合
$ npm install -g @google/clasp
# yarnの場合
$ yarn global add @google/clasp
初回は認証します。
$ clasp login
以下のようなページが開くので、一番下までスクロールして承認すればOK。
色々コマンド弄りたいのでgasライブラリも入れておきます。
$ yarn add @types/google-apps-script
また、今回typescriptで作るので色々いじれるようにします。
$ yarn add eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser prettier eslint-config-prettier eslint-plugin-prettier
$ yarn run eslint --init
プロジェクトの作成
今回は既存のスプレッドシートに対し色々できるようなスクリプトを作ることがゴールです。
なので以下のようなコマンドを実行します。
$ clasp create --title "clasp-test" --rootDir "./src"
? Create which script? standalone
Created new standalone script: https://script.google.com/d/***/edit
Warning: files in subfolder are not accounted for unless you set a '.claspignore' file.
Cloned 1 file.
└─ ./src/appsscript.json
$ tree -L 2
.
├── src
│ └── appsscript.json
└── yarn.lock
* 余分なのは消してます
そうすると以下のようなファイルが作られるので内容を確認します。
$ cat .clasp.json
{
"scriptId": "***",
"rootDir": "./src"
}
$ cat src/appsscript.json
{
"timeZone": "America/New_York",
"dependencies": {
},
"exceptionLogging": "STACKDRIVER"
}
この時点で https://script.google.com/home を見ると新しくプロジェクトが作成されていることが確認できます。
ここで、一旦プログラムが動くことを確認します。
今回 src
配下をルートディレクトリに設定したので、src配下に適当にプログラムを作ります。
// 雑にメッセージを出すだけ。
function main() {
Logger.log("Hello clasp!");
}
ありがたいことにclaspは標準でtypescriptをサポートしてくれているので通常のデプロイコマンドにてデプロイできます。
$ clasp push
該当のプロジェクトを見ると次のようにトランスパイルされていることがわかります。
この状態で実行してみると、ちゃんと想定通り動いてくれていることがわかります。
あとは好きにプログラムを書くだけ。
スプレッドシートから値を取ってみる
で、このスプレッドシートから値を取るわけですが、プログラム上にハードコードはしたくないですよね。
そこで、GASでは秘匿情報をプロパティとして設定できるので、その機能を利用します。
今回はシートのID(URLの https://docs.google.com/spreadsheets/d/<これ>/edit#gid=0
)をSHEET_ID
として保存します。
で、折角なのでクラス分割します。
プロパティを取得し返却するクラスを用意します。
export class Properties {
private sheetID: string;
constructor() {
this.sheetID = PropertiesService.getScriptProperties().getProperty('SHEET_ID');
}
getSheetID(): string {
return this.sheetID;
}
}
値を取得して中身を見るようにプログラムを改変します。
import { Properties } from "./model/properties";
function main() {
const p = new Properties();
this.sheets = SpreadsheetApp.openById(p.getSheetID()).getSheets();
// とりあえず全部で何行あるかを確認する
const indexColumns = this.sheets[0].getRange('A:A').getValues();
const lastRow = indexColumns.filter(String).length;
// 値が存在する範囲で末端までの全要素を出す
const values = this.sheets[0].getRange(1, 1, lastRow, 1).getValues();
Logger.log(values);
}
にゃお。
Slackに投げる
中間ロジックは一旦置いておいて、slackに結果を投げます。
slack側に投げるための設定(incoming webhook)などは省略します。
SlackAppの利用
GAS ⇆ Slackの連携については、SlackAppのライブラリを利用します。
一応プロジェクトにて リソース > ライブラリ
からインポートできます
READMEにあるように M3W5Ut3Q39AaIwLquryEPMwV62A3znfOO
を設定すれば使えるようにはなります。
ただ、デプロイのたびに消えてしまって不便なのでプロジェクト側に記載します。
ついでに日本にタイムゾーンを揃えときます
$ cat src/appsscript.json
{
"timeZone": "Asia/Tokyo",
"dependencies": {
"libraries": [
{
"userSymbol": "SlackApp",
"libraryId": "M3W5Ut3Q39AaIwLquryEPMwV62A3znfOO",
"version": "22"
}
]
},
"exceptionLogging": "STACKDRIVER"
}
SlackAppのinterfaceを定義してあげることでローカルでも開発しやすくしています。
declare let SlackApp: SlackApp;
interface SlackAppAPI {
postMessage(channelId: string, message: string): void;
}
interface SlackApp {
create(token: string): SlackAppAPI;
}
export class Slack {
private slack: SlackAppAPI;
private channelID: string;
constructor(accessToken: string, channelID: string) {
this.slack = SlackApp.create(accessToken);
this.channelID = channelID;
}
post(message): void {
this.slack.postMessage(this.channelID, message);
}
}
this.slack.postMessage(this.channelID, message);
最終的にはpostMessageで投げつけるだけです。
これが使えるように index.ts
と properties.ts
を更新します。
import { Properties } from "./model/properties";
import { Slack } from './model/slack';
function main() {
const p = new Properties();
this.sheets = SpreadsheetApp.openById(p.getSheetID()).getSheets();
// とりあえず全部で何行あるかを確認する
const indexColumns = this.sheets[0].getRange('A:A').getValues();
const lastRow = indexColumns.filter(String).length;
// 値が存在する範囲で末端までの全要素を出す
const values = this.sheets[0].getRange(1, 1, lastRow, 1).getValues();
const slack = new Slack(p.getAccessToken(), p.getChannelID());
slack.post('今が旬の課題です');
for (let val of values) {
slack.post(val[0]);
}
}
slackの機微情報もプロパティに入れておきます。
export class Properties {
private sheetID: string;
private accessToken: string;
private channelID: string;
constructor() {
this.sheetID = PropertiesService.getScriptProperties().getProperty('SHEET_ID');
this.accessToken = PropertiesService.getScriptProperties().getProperty('ACCESS_TOKEN');
this.channelID = PropertiesService.getScriptProperties().getProperty('CHANNEL_ID');
}
getSheetID(): string {
return this.sheetID;
}
getAccessToken(): string {
return this.accessToken;
}
getChannelID(): string {
return this.channelID;
}
}
実行してみると
ねこのもり!
あとは独自のロジックで課題を重み付けして、適当にフォーマットしたものを定期的にslackに投げるだけ!
ここはシートの中身に密接に関わってしまうので今回書きません。
まとめ
今回の記事の内容をgithubにあげておきました。
https://github.com/mochisuna/clasp-test
あと、社内で発表したのでSpeaker Deckに資料上ました
参考になれば!