Slack + GASで社内仮想通貨取引所を実装 Part1

# 概要
社内コミュニケーションの活性化やモチベーションアップに利用される社内通貨を、Slack上でやり取りできるようにする。

# 背景
昨今、[いくつかの企業](https://hcm-jinjer.com/media/contents/b-contents-6407/)で社内通貨を導入して、社員同士の交流を深めたり、貢献度を見える化したりする取り組みがあります。確かにぼくも普段、仕事のお世話になったメンバーに対してブラックサンダーをお布施するなどしていますが、特に切羽詰っているときは先送りになってしまったり、それほどではないものの、軽い感謝の気持ちを何かSlackのEmoji以外で表現できないかと思っていました。

ただ、その社内通貨のためにServerからFrontまで実装するのは大変ですし、普段のコミュニケーションツールと別個にシステムが存在するのも利便性を損なうということから、[Slack](https://slack.com/intl/ja-jp)の[Slash Command](https://api.slack.com/slash-commands)と[Google Apps Script](https://developers.google.com/apps-script/)を組み合わせて簡単な仮想通貨取引ツールを作成しました。

# 成果物
口座を作って、
![image.png](https://qiita-image-store.s3.amazonaws.com/0/234668/06ec3244-288f-9bd1-96b0-d090738c0bef.png)
ユーザーを指定して送金して、
![図2.png](https://qiita-image-store.s3.amazonaws.com/0/234668/0e5c6226-5af4-3907-fc66-bdc5f2a31fae.png)
残高確認 :v:
![image.png](https://qiita-image-store.s3.amazonaws.com/0/234668/b9cfcc13-67da-69ee-cf45-5ba23cac11e4.png)

# 実装解説
## 全体構成
全体の構成図はこんな感じです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/234668/e8606ca4-932f-ac4d-afdd-41f4c10790a2.png)

SlackのSlash Commandを利用するので、そのAPIの受け口用にGASを利用するという構成になっています。認証周りに関しては 「[Slash CommandsとGASでSlackのオリジナルコマンドをつくる](https://qiita.com/chikuwa111/items/7a1a349b82318a5861cc)」 がとてもわかりやすく参考になります。

基本的なPost処理に関してはそちらの記事に書いてあるので、ここではどのようにGASから[Google Sheets](https://www.google.com/intl/ja_jp/sheets/about/)を操作したのかについて述べたいと思います。

## Google Sheetsの触り方
口座を管理するシートは以下のようなユーザー名と残高のみを保持した簡単な構成です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/234668/9b5de461-27b2-acfb-7c53-25f6283d8649.png)

Google SheetsをGASから触るときには[SpreadsheetApp](https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet-app)を利用するのが良いです。公式ドキュメントを読むと他のやり方もいろいろとあり混乱しますが、とにかくこれが一番書きやすかったです。そして、以下の要領でSheetにアクセスできるようになります。

```js:Utilities.gs
function getLedger() {
  /*
  Return Google Sheets storing balance of users.
  */
  return SpreadsheetApp.openById(PropertiesService.getScriptProperties().getProperty("SHEETID")).getSheets()[0]
}
```

SheetIDは「File > Project properties > Script Properties」に登録しておくと、コードが汎化するのでうま味です。

そして、Sheetへの入出力は基本[`sheet.getRange`](https://developers.google.com/apps-script/reference/spreadsheet/sheet#getrangerow-column-numrows-numcolumns)で領域を選択してから、そこに対してgetterとsetterを適用するという流れになります。Indexは**1-origin**なので、普段[MATLAB](https://jp.mathworks.com/products/matlab.html)でも使っていない限り注意してください。

なので、この場合は

1. 対象ユーザーのSheet上でのIndexを走査して探す
2. そのIndexの残高カラムに対してgetter及びsetterを適用する

という形で行っています。

### Sheetの走査
```js:Utilities.gs
function getUserIndex(users) {
  /*
  Return associative array of {"user": "index in Google Sheets"}.
  If not found, the value becomes NaN.
  */
  const sheet = getLedger()
  const memberList = sheet.getRange(2, 1, sheet.getLastRow() - 1, 1).getValues()

  // Find user from "users" and push them to "userIndex" associative array
  const userIndex = {}
  for (var u in users) {
    userIndex[users[u]] = NaN // Set default value
    for (var m in memberList) {
      if (users[u] === memberList[m][0]) { // User found in the member list
        userIndex[users[u]] = parseInt(m) + 2 // +2 because the sheet has the header row and Google Sheet is 1-origin.
      }
    }
  }
  return userIndex
}
```

[`sheet.getLastRow`](https://developers.google.com/apps-script/reference/spreadsheet/sheet#getlastrow)で**値のある**最後の行数を得ることができて便利です。範囲を指定して上からfor loopで走査して該当した行数を連想配列に突っ込んで返すという形です。

## 該当セルにgetter及びsetterの適用
```js:SendCoin.gs
const userIndex = getUserIndex(recipients.concat([sender]))

const notFoundUsers = [] // If a recipient does not have acccount yet, notify later.
const ledger = getLedger()
for (var i in recipients) { // Add coins to each recipient
    const r = recipients[i]
    if (isNaN(userIndex[r])) { // Not member yet.
        notFoundUsers.push(r)
    } else {
        const v = ledger.getRange(userIndex[r], 2).getValue()
        ledger.getRange(userIndex[r], 2).setValue(v + amount)
    }
}

// Subtract total amount of sent coins
const current = ledger.getRange(userIndex[sender], 2).getValue()
const numPayedUsers = recipients.length - notFoundUsers.length
const payment = amount * numPayedUsers
ledger.getRange(userIndex[sender], 2).setValue(current - payment) // Update balance
```

[`getValue`](https://developers.google.com/apps-script/reference/spreadsheet/range#getvalue)と[`setValue`](https://developers.google.com/apps-script/reference/spreadsheet/range#setvaluevalue)を使って、現在の値を取得した後、差分を走査して再び値を設定しています。

## 実装公開してます
この記事で全てを説明するのは大変なので、GAS実装は[GitHubで公開](https://github.com/smn-ailab/hasoya-qiita/tree/master/internal_virtual_currency)しています。それぞれのファイルの中身をコピペして、`Script properties`に

- DEFAULTAMOUNT: 口座開設時の初期残高
- SHEETID: 口座情報が入ったGoogle SheetsのID
- SLACKTOKEN: SlackのAppにある `Verification Token`

を設定すれば動くはずです。

# 機能追加予定
- オークション機能: そもそもやりたかったこと
- 税金機能: 明確なAssignがない公共事業的な仕事への報酬
- ブロックチェーン :chains: : 流行りだし

# あとがたり
普段はデータサイエンスを仕事としてやっているので、Python :snake: がメインなのと、Javascriptは書くこともあってもD3.jsのためだけに触るので、JSの仕様に慣れるのが大変でした…また、JSにはあるはずの[`Arrow Functions`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions)が使えないなど、最近の言語仕様に甘えきったコードを書くぼくには面倒くさく感じるシーンが多かったです。この辺についてはまたどこかでまとめようと思います。それでは :wave: