slackでメンション飛ばしてうさぎのエサと体重を記録する

# 概要
* ウサギを飼うときの体重、チモシー、ペレット（通称カリカリ）の記録をslackでやる
* 体重とチモシーとペレットの平均値と標準偏差を予め入力しておくことで、入力された数値からいずれに該当するか自動的に判定します。
* slackからのメンションをgoogle cloud functionで受けて、google datastoreに保存して、slackにメッセージします。

# 動作イメージ
[![Image from Gyazo](https://i.gyazo.com/e4e3cdd2734c4d7c2eb929c1ba77aeea.gif)](https://gyazo.com/e4e3cdd2734c4d7c2eb929c1ba77aeea)

# 手順
* [google cloud datastoreをセットアップする](https://cloud.google.com/datastore/docs/datastore-api-tutorial?hl=ja#datastore-datastore-add-entity-nodejs)
    * 体重、チモシー、カリカリのパラメータ（平均値、標準偏差）を入力しておく
        * ![image.png](https://qiita-image-store.s3.amazonaws.com/0/124445/ee766b6a-beed-15e1-fdd3-fcf2c2d0dc3f.png)

* google cloud functionを作り、下記のコードをdeployする
* [slack](https://api.slack.com/apps) の Create New APP で BOTを作る。手順はこちらの[Cloud Functionsで作る選択肢つき Slack BOT](https://qiita.com/castaneai/items/144bb17ec8bb21f306d4) がとても詳しいです！


```index.js
const PROJECT_ID = 'google datastoreのプロジェクトID';
const BOT_USER_TOKEN = "slackのBot User OAuth Access Token";
const GOOGLE_APPLICATION_CREDENTIALS = "./アプリケーションのデフォルト認証情報.json"; // https://cloud.google.com/docs/authentication?hl=ja

const startOfToday = require('date-fns/start_of_today');
const format = require('date-fns/format')
const request = require('request-promise-native');
// const payloadTest = require('./payloadTest.json'); テスト用
const Datastore = require('@google-cloud/datastore');
const datastore = Datastore({projectId: PROJECT_ID});

// 環境変数を設定する（簡易的です。.envでやりましょうー）
const set_env = () => {
  process.env.BOT_USER_TOKEN = BOT_USER_TOKEN;
  process.env.GOOGLE_APPLICATION_CREDENTIALS = GOOGLE_APPLICATION_CREDENTIALS;
}

// slackに返信する
const postMessage = (payload) => {
  request.post('https://slack.com/api/chat.postMessage', {
    headers: {'Authorization': `Bearer ${process.env.BOT_USER_TOKEN}`},
    json: payload,
  });
}

// いわゆる。zスコア計算する。小さいほど、その（体重、チモシー、カリカリ）分布から得られる値と判断する
// 今回は不要だけど累積分布関数でp値だせばよりそれっぽい。
const zScore = (x, mean, stdev) => {
  const z = (x - mean) / stdev;
  return z
}

// 入力値を保存する
async function addRecord(kind, value, unit, message) {
  const date = startOfToday();
  const dateStr = format(date, 'YYYY-MM-DD');
  const recordKey = datastore.key([kind, dateStr]);
  const data = [
    {
      name: 'date',
      value: date,
    },
    {
      name: 'unit',
      value: unit,
    },
    {
      name: 'value',
      value: value,
    },
    {
      name: 'message',
      value: message,
      excludeFromIndexes: true,
    },
  ];
  const entity = {
    key: recordKey,
    data: data,
  };
  datastore.save(entity);
}


// 体重、チモシー、カリカリのパラメータ（平均値、標準偏差）を取得する
const getEntities = (kind) => {
  const query = datastore.createQuery(kind)
    .limit(10);
  return datastore.runQuery(query)
}

const onRequest = async (req, res) => {
  // slackからの入力値を取得する
  let payload = req.body;
  // let payload = payloadTest;

  // APIキーなどを環境変数にセットする
  set_env();

  // slackからのverificationのため。
  // https://qiita.com/castaneai/items/144bb17ec8bb21f306d4
  if (payload.type === 'url_verification') {
    return res.status(200).json({'challenge': payload.challenge});
  }

  let kind = '';
  let z = 9999;
  const message = payload.event.text
  const value = message.match(/> [^0-9]*([0-9]+[\.]?[0-9]*)/) && message.match(/> [^0-9]*([0-9]+[\.]?[0-9]*)/)[1]; // 数値を抽出

  // valueに数値が入っていない場合は何もせず返す
  if (!value || !(value > 0) || !(payload.event.type === 'app_mention')) {
    postMessage({
      text: `<@${payload.event.user}> 数字入れてね`,
      channel: payload.event.channel,
    });
    return res.status(200).send('OK');
  }

  // 数値が 体重、チモシー、カリカリのどの平均値に近いか判定して
  // slackで返事 & データを保存
  getEntities('Parameter')
    .then(results => {
      const entities = results[0];
      let selectedEntity = {};
      entities.forEach(entity => {
        if (Math.abs(z) > Math.abs(zScore(value, entity.mean, entity.stdev))) {
          selectedEntity = entity;
          z = zScore(value, entity.mean, entity.stdev);
        }
      });
      addRecord(selectedEntity.kind, value, 'g', message);
      const date = startOfToday();
      const dateStr = format(date, 'YYYY-MM-DD');
      postMessage({
        text: `<@${payload.event.user}> 入力ありがと。${dateStr}の ${selectedEntity.name} は ${value} g で記録しておくね。`,
        channel: payload.event.channel,
      });
    });

  return res.status(200).send('OK');
}

exports.slackChoicesBot = onRequest;
```

以上。