Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails以外の開発一般

TS: TODOリストとSlackを連携させよう!その1「更新通知」

🔗目次

  1. まえがき
  2. 今回作りたいもの
  3. 完成品
  4. 環境構築
  5. Slack Appを完成させる
  6. GAS側を完成させる
  7. Slackアプリをデプロイする
  8. あとがき

🔗まえがき

皆さんどうもこんにちは、この度晴れて株式会社ECN所属インターンから正社員となりましたFuseです。
株式会社ECNでは近頃、特に納期がキツくないけど誰でもいいから手が空いたらやって欲しいという社内の仕事を書き込んで管理したり依頼したり受けたりするTODOリストが登場しました。私もちょくちょく確認しては、受けれそうな仕事を受けて提出したり、様々な提案を書き込んだりしています。

ですが現状依頼の書き込みやステータスの更新を通知する手段がなく、いちいちスプレッドシートに見に行く必要があります。新しい依頼の追加や完了報告もスプレッドシートからでしか行えません。

そこで今回はSlackに依頼が追加された、または内容が更新されたときにSlack上に通知する仕組みを作っていきます。
↑目次に戻る

🔗今回作りたいもの

  • 必須のカラムが埋まって、ステータスが"下書き",""(空)以外になったとき新しいレコードの情報をSlackに通知する
  • カラムの内容が更新されたとき、ID、タイトル、変更されたカラム、変更前後の値を通知する

    ↑目次に戻る

🔗完成品

↑目次に戻る

🔗環境構築

まずはSlack用アプリを作るための環境構築を行います。
公式のクイックスタートを参考にしながら構築していきましょう。

ステップ1 SlackCLIのインストール

まずは重要なツールであるSlackCLIをインストールします。
私はWindowsを使用しているので

irm https://downloads.slack-edge.com/slack-cli/install-windows.ps1 -outfile 'install-windows.ps1'

を実行します。macやLinuxをご使用の場合は

curl -fsSL https://downloads.slack-edge.com/slack-cli/install.sh | bash

を実行してください。
これでSlackCLIとついでに次世代プラットフォームのランタイム環境であるDenoなどの必要なあれこれが一緒にインストールされます。
管理者権限を与えないとうんともすんとも言わないので気をつけましょう。(1敗)

ステップ2 SlackCLIを承認しよう

SlackCLIのインストールが終わったらワークスペースで Slack CLI を認証してあげる必要があります。
コンソールにslack loginと入力すると認証コードが

/slackauthticket "認証コード"

の形で排出されます。それを適当なチャンネルに投稿し、表示された指示に従うと8文字の英数字が表示されるのでそれをコンソールに入力してやれば認証は完了です。
成功すると

You've successfully authenticated! >
Authorization data was saved to C:\Users\"ユーザー名".slack\credentials.json
Get started by creating a new app with slack.exe create my-app
Explore the details of available commands with slack.exe help

と表示され認証成功です。
VSCode+PowerShellを使ってる方はパスが通ってないことがあるので

[Environment]::SetEnvironmentVariable("Path", $ENV:Path + "C:\Users\1219m\AppData\Local\slack-cli\bin", [EnvironmentVariableTarget]::Machine)

で通しましょう。
↑目次に戻る

🔗Slack Appを完成させる

早速SlackAppを作っていきます。まずはチャンネルIDやスプシのURLを格納する.envファイルを作ります。

.env

CHANNEL_ID="通知先のチャンネルのID"
SPREADSHEET_URL="スプレッドシートのURL"

.envを用意したらまずはトリガーを叩いたときに実行されるワークフローを作りましょう。

workflows\QuestCreatedWorkFrow.ts

import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";
import { load } from "https://deno.land/std@0.203.0/dotenv/mod.ts";
await load({ export: true });
const SHEETURL = Deno.env.get("SPREADSHEET_URL");
const CHANNEL_ID = Deno.env.get("CHANNEL_ID");
export const QuestCreatedWorkFrow = DefineWorkflow({
  callback_id: "quest_created",
  title: "quest_created",
  description: "Send a notice to channel",
  input_parameters: {
    properties: {
      id: {
        type: Schema.types.string,
      },
      target: {
        type: Schema.types.string,
      },
      title: {
        type: Schema.types.string,
      },
      bunya: {
        type: Schema.types.string,
      },
      teishutu: {
        type: Schema.types.string,
      },
      owner: {
        type: Schema.types.string,
      },
      limit: {
        type: Schema.types.string,
      },
    },
    required: ["title", "owner", "teishutu", "bunya", "target", "id"],
  },
});
const mes = `皆さん、新しい依頼が追加されましたよ!\n` +
  "```\n" +
  `*行番号*\n` +
  `${QuestCreatedWorkFrow.inputs.id}\n` +
  `*対象*\n` +
  `${QuestCreatedWorkFrow.inputs.target}\n` +
  `*タイトル*\n` +
  `${QuestCreatedWorkFrow.inputs.title}\n` +
  `*分野*\n` +
  `${QuestCreatedWorkFrow.inputs.bunya}\n` +
  `*担当者*\n` +
  `${QuestCreatedWorkFrow.inputs.owner}\n` +
  `*提出方法*\n` +
  `${QuestCreatedWorkFrow.inputs.teishutu}\n` +
  `*期限*\n` +
  `${QuestCreatedWorkFrow.inputs.limit}\n` +
  "```\n" +
  `*<${SHEETURL}|早速見に行きませんか?>*`;
QuestCreatedWorkFrow.addStep(Schema.slack.functions.SendMessage, {
  channel_id: CHANNEL_ID ?? "",
  message: mes,
});

ワークフロー作成の流れは以下のようになっています。
1. 定義したDefineWorkflow関数でワークフローの名前や説明、入力などを定義
2. ワークフローのaddStepメソッドでワークフローに手順を追加

送信先のチャンネルのIDや投稿に埋め込むスプシのURLは.envから取得しています。

workflows\QuestCreatedWorkFrow.ts

import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";
import { load } from "https://deno.land/std@0.203.0/dotenv/mod.ts";
await load({ export: true });
const SHEETURL = Deno.env.get("SPREADSHEET_URL");
const CHANNEL_ID = Deno.env.get("CHANNEL_ID");
export const QuestUpdatedWorkFrow = DefineWorkflow({
  callback_id: "quest_updated",
  title: "quest_updated",
  description: "Send a notice to channel",
  input_parameters: {
    properties: {
      id: {
        type: Schema.types.string,
      },
      target: {
        type: Schema.types.string,
      },
      title: {
        type: Schema.types.string,
      },
      column: {
        type: Schema.types.string,
      },
      oldval: {
        type: Schema.types.string,
      },
      newval: {
        type: Schema.types.string,
      },
    },
    required: ["title", "column", "oldval", "newval", "target", "id"],
  },
});
const mes = `皆さん、依頼の内容が更新されました!\n` +
  "```\n" +
  `*行番号*\n` +
  `${QuestUpdatedWorkFrow.inputs.id}\n` +
  `*対象*\n` +
  `${QuestUpdatedWorkFrow.inputs.target}\n` +
  `*タイトル*\n` +
  `${QuestUpdatedWorkFrow.inputs.title}\n` +
  `*更新されたカラム*\n` +
  `${QuestUpdatedWorkFrow.inputs.column}\n` +
  `*更新内容*\n` +
  `"${QuestUpdatedWorkFrow.inputs.oldval}"→"${QuestUpdatedWorkFrow.inputs.newval}"\n` +
  "```\n" +
  `*<${SHEETURL}|早速見に行きませんか?>*`;
QuestUpdatedWorkFrow.addStep(Schema.slack.functions.SendMessage, {
  channel_id: CHANNEL_ID,
  message: mes,
});

クエスト更新時のワークフローも同様に作っていきます。
内容はほとんど同じですが入力内容とメッセージ生成用のテンプレートの内容が違います。
ワークフローを作ったら忘れずにmanifest.tsに登録しておきましょう。

manifest.ts

import { Manifest } from "deno-slack-sdk/mod.ts";
import { QuestCreatedWorkFrow } from "./workflows/QuestCreatedWorkFrow.ts";
import { QuestUpdatedWorkFrow } from "./workflows/QuestUpdatedWorkFrow.ts";

/**
 * The app manifest contains the app's configuration. This
 * file defines attributes like app name and description.
 * https://api.slack.com/future/manifest
 */
export default Manifest({
  name: "ECN-Quest-Board",
  description: "手が空いた時用のTODOリストの追加や更新を通知します。",
  icon: "assets/Icon.png",
  functions: [],
  workflows: [QuestCreatedWorkFrow, QuestUpdatedWorkFrow],//ここにexportしたワークフローを追加する
  outgoingDomains: [],
  botScopes: ["commands", "chat:write", "chat:write.public"],
});

その後はトリガーを作っていきます。今回使うのはWebHookトリガー、外部からのWebリクエストにより発火します。

triggers\QuestCreated.ts

import { Trigger } from "deno-slack-api/types.ts";
import { QuestCreatedWorkFrow } from "../workflows/QuestCreatedWorkFrow.ts";
import { TriggerTypes } from "deno-slack-api/mod.ts";

const trigger: Trigger<typeof QuestCreatedWorkFrow.definition> = {
  type: TriggerTypes.Webhook,
  name: "Created Quest Notice to Channel",
  description: "runs the workflow",
  workflow: "#/workflows/quest_created",
  inputs: {
    id: {
      value: "{{data.id}}",
    },
    target: {
      value: "{{data.target}}",
    },
    title: {
      value: "{{data.title}}",
    },
    bunya: {
      value: "{{data.bunya}}",
    },
    teishutu: {
      value: "{{data.teishutu}}",
    },
    owner: {
      value: "{{data.owner}}",
    },
    limit: {
      value: "{{data.limit}}",
    },
  },
};

export default trigger;

トリガーを作る際は

  • トリガーの種別
  • 名前
  • 説明文
  • 実行するワークフロー
  • 入力の定義

トリガーの種別については聞きなれないと思われるので解説します。

トリガーの種別について

トリガーの種類は4種類あります。

名前 TriggerTypes 解説
ショートカット TriggerTypes.Shortcut スラッシュコマンドやボタンで発火
イベント TriggerTypes.Event Slack内でのイベントで発火
スケジュール TriggerTypes.Scheduled 一定時間ごとに発火
Webhook TriggerTypes.Webhook 外部からのWebリクエストにより発火

今回はWebhookトリガーを使いたいのでTriggerTypes.Webhookを指定します。
その後はクエスト更新時のトリガーも同様に作っていきましょう。

triggers\QuestUpdated.ts

import { Trigger } from "deno-slack-api/types.ts";
import { QuestUpdatedWorkFrow } from "../workflows/QuestUpdatedWorkFrow.ts";
import { TriggerTypes } from "deno-slack-api/mod.ts";

const trigger: Trigger<typeof QuestUpdatedWorkFrow.definition> = {
  type: TriggerTypes.Webhook,
  name: "Updated Quest Notice to Channel",
  description: "runs the workflow",
  workflow: "#/workflows/quest_updated",
  inputs: {
    id: {
      value: "{{data.id}}",
    },
    target: {
      value: "{{data.target}}",
    },
    title: {
      value: "{{data.title}}",
    },
    column: {
      value: "{{data.column}}",
    },
    oldval: {
      value: "{{data.oldval}}",
    },
    newval: {
      value: "{{data.newval}}",
    },
  },
};

export default trigger;

↑目次に戻る

🔗GAS側を完成させる

WebHookを受け取る側が完成したので次はWebHookにリクエストを投げる側のプログラムを作っていきます。
あらかじめ作られていたスプレッドシートを開き、
拡張機能>Apps Scriptでスプレッドシートに紐づいたGASプロジェクトを新規作成できます。

あとはこの記事で紹介した方法でスプレッドシートの更新を検知できるようにしましょう。

GAS: Slackからフォーム入力でDocBaseのメモを生成したい


出来上がったコードはこちらになります。

function onEditCell(e) { // セルが編集されたら実行
  console.log(e.source.getSheetName())
  if(e.source.getSheetName()!="やって欲しいことリスト") return; // 編集されたシートが一時作業用シートでなければキャンセル
  const ActiveSheet=e.source.getSheetByName("やって欲しいことリスト"); // 一時作業用シートをオブジェクトとして取得
  console.log(e.source.getSheetName())
  if(e.range.getRow()<=2)return//見出し行以上ならキャンセル
  const range = ActiveSheet.getRange(e.range.getRow(),1,1,12); // 編集された行を取得
  const data=range.getValues()[0]//データ取得
  console.log(data)
  if(data[1]=="") return; // 「対象」は入力されているか?
  if(data[2]=="") return; // 「内容」は入力されているか?
  if(data[3]=="") return; // 「分野」は入力されているか?
  if(data[5]=="") return; // 「担当者」は入力されているか?
  if(data[7]=="") return; // 「状態」は入力されているか?
  if(data[7]=="下書き"||data[7]=="キャンセル") return; // 「状態」は下書きやキャンセルでないか?
  if(data[8]=="") return; // 「提出方法」は入力されているか?
  if(data[10]=="") return; // 「期日」は入力されているか?
  if(data[11]==""){//まだ通知されてないなら
    NoticeCreatedQuest(data)
    const targetcell = ActiveSheet.getRange(range.getRow(),12); // 編集された行を取得
    targetcell.setValue("済")
  }else{
    const columnname=ActiveSheet.getRange(2,e.range.getColumn()).getValues()[0][0]
    NoticeUpdatedQuest(data,columnname,e.oldValue,e.range.getValues()[0][0])
  }
}
function NoticeCreatedQuest(data){
  const url = PropertiesService.getScriptProperties().getProperty('CREATED_WEBHOOK') // トークンを取得
  var headers = { // HTTPリクエストのヘッダー
    "Accept": "application/json",
    "Content-type": "application/json"
  }

  var data = { // データ部
    "id":data[0].toString(),
    "target":data[1],
    "title":data[2],
    "bunya":data[3],
    "teishutu":data[8],
    "owner":data[5],
    "limit":data[10]
  }

  var options = { // リクエストのオプション(メソッド・データ・ヘッダー)
    "method": "post",
    "payload": JSON.stringify(data),
    "headers": headers
  };
  var resp=UrlFetchApp.fetch(url, options);//リクエストを送信
}
function NoticeUpdatedQuest(data,column_name,oldval,newval){
  const url = PropertiesService.getScriptProperties().getProperty('UPDATED_WEBHOOK') // トークンを取得
  var headers = { // HTTPリクエストのヘッダー
    "Accept": "application/json",
    "Content-type": "application/json"
  }

  var data = { // データ部
    "id":data[0].toString(),
    "target":data[1],
    "title":data[2],
    "column":column_name,
    "oldval":oldval,
    "newval":newval
  }

  var options = { // リクエストのオプション(メソッド・データ・ヘッダー)
    "method": "post",
    "payload": JSON.stringify(data),
    "headers": headers
  };
  var resp=UrlFetchApp.fetch(url, options);//リクエストを送信
}

12列目の通知済みフラグの値の有無に応じて異なるエンドポイントにデータを送信しています。また、WebHookのURLはハードコーディングするのではなくGASのスクリプトプロパティという機能を使って格納、取得しています。
↑目次に戻る

🔗 Slackアプリをデプロイする

完成したslackアプリは

slack deploy

でデプロイできます。
アプリデプロイ後はslack trigger createの選択肢にDeployedが増えます。
slack env addで環境変数を本番環境に設定して、
slack trigger createで2つのトリガーをデプロイしてやれば完成です。
GASのスクリプトプロパティも忘れずに本番環境のURLに変えておきましょう。
デプロイしたことでアイコンもmanifest.tsに設定したものに変更されます。

これにて完成です。お疲れさまでした!

↑目次に戻る

🔗あとがき

これにてこの記事はおわりです。
次回はクエストの一覧の確認と受注、新規登録をSlackから行えるようにしていきます。
↑目次に戻る


株式会社ECNはPHP、JavaScriptを中心にお客様のご要望に合わせたwebサービス、システム開発を承っております。
ビジネスの最初から最後までサポートを行い
お客様のイメージに合わせたWebサービス、システム開発、デザインを行います。

関連記事

TS&React: three-vrmを使って女の子に読み聞かせしてもらう

GAS: Slackからフォーム入力でDocBaseのメモを生成したい


関連記事

該当する記事がありません。

CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。