13
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ギフティAdvent Calendar 2019

Day 4

clasp + TypeScriptで課題改善botを作った

Last updated at Posted at 2019-12-04

なにこれ

この記事はギフティ 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。
スクリーンショット 2019-12-04 14.19.24.png

色々コマンド弄りたいので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配下に適当にプログラムを作ります。

src/index.ts
// 雑にメッセージを出すだけ。
function main() {
    Logger.log("Hello clasp!");
}

ありがたいことにclaspは標準でtypescriptをサポートしてくれているので通常のデプロイコマンドにてデプロイできます。

$ clasp push

該当のプロジェクトを見ると次のようにトランスパイルされていることがわかります。

スクリーンショット 2019-12-04 15.27.55.png

この状態で実行してみると、ちゃんと想定通り動いてくれていることがわかります。
スクリーンショット 2019-12-04 15.34.32.png

あとは好きにプログラムを書くだけ。

スプレッドシートから値を取ってみる

適当に値を入れときます。
スクリーンショット 2019-12-04 15.51.42.png

で、このスプレッドシートから値を取るわけですが、プログラム上にハードコードはしたくないですよね。

そこで、GASでは秘匿情報をプロパティとして設定できるので、その機能を利用します。
スクリーンショット 2019-12-04 15.40.59.png

今回はシートのID(URLの https://docs.google.com/spreadsheets/d/<これ>/edit#gid=0)をSHEET_IDとして保存します。
スクリーンショット 2019-12-04 15.42.24.png

で、折角なのでクラス分割します。
プロパティを取得し返却するクラスを用意します。

src/model/properties.ts
export class Properties {
    private sheetID: string;
    constructor() {
        this.sheetID = PropertiesService.getScriptProperties().getProperty('SHEET_ID');
    }
    getSheetID(): string {
        return this.sheetID;
    }
}

値を取得して中身を見るようにプログラムを改変します。

src/index.ts
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);
}

デプロイして挙動の確認。
スクリーンショット 2019-12-04 16.10.50.png
スクリーンショット 2019-12-04 16.11.04.png

にゃお。

Slackに投げる

中間ロジックは一旦置いておいて、slackに結果を投げます。
slack側に投げるための設定(incoming webhook)などは省略します。

SlackAppの利用

GAS ⇆ Slackの連携については、SlackAppのライブラリを利用します。

一応プロジェクトにて リソース > ライブラリ からインポートできます
スクリーンショット 2019-12-04 16.26.06.png

READMEにあるように M3W5Ut3Q39AaIwLquryEPMwV62A3znfOO を設定すれば使えるようにはなります。
スクリーンショット 2019-12-04 16.26.44.png
ただ、デプロイのたびに消えてしまって不便なのでプロジェクト側に記載します。

ついでに日本にタイムゾーンを揃えときます

$ cat src/appsscript.json
{
  "timeZone": "Asia/Tokyo",
  "dependencies": {
    "libraries": [
      {
        "userSymbol": "SlackApp",
        "libraryId": "M3W5Ut3Q39AaIwLquryEPMwV62A3znfOO",
        "version": "22"
      }
    ]
  },
  "exceptionLogging": "STACKDRIVER"
}

SlackAppのinterfaceを定義してあげることでローカルでも開発しやすくしています。

src/model/slack.ts
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.tsproperties.tsを更新します。

src/index.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の機微情報もプロパティに入れておきます。

src/model/properties.ts
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;
    }
}

実行してみると

スクリーンショット 2019-12-04 17.17.09.png

ねこのもり!

あとは独自のロジックで課題を重み付けして、適当にフォーマットしたものを定期的にslackに投げるだけ!
ここはシートの中身に密接に関わってしまうので今回書きません。

まとめ

今回の記事の内容をgithubにあげておきました。
https://github.com/mochisuna/clasp-test

あと、社内で発表したのでSpeaker Deckに資料上ました

参考になれば!

13
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?