App Home のタブを Cloud Functions で

構築した話

自己紹介

  • @hey_cube
  • 株式会社オプト
  • React / Rails / GCP

今日話すこと

  • App Home とは
  • どんなものを作ったか
  • 実装の紹介
  • 開発を振り返って
  • まとめ

App Home とは

  • ユーザーと Slack を 1 対 1 で繋ぐスペース
  • ユーザーに直感的にアプリを使ってもらうために追加された
  • About・Messages・Home タブが存在する

どんなものを作ったか

  • 社内限定で配信している YouTube Live のプッシュ通知を Messages タブで送る
  • アーカイブの再生リストを Home タブに表示する

実装の紹介

  • TypeScript
  • Google Cloud Functions
  • Google Apps Script

package.json

  "devDependencies": {
    "@google-cloud/functions-emulator": "^1.0.0-beta.6",
    "@types/follow-redirects": "^1.8.0",
    "@types/node": "^13.7.1",
    "ts-node-dev": "^1.0.0-pre.44",
    "typescript": "^3.7.5"
  },
  "dependencies": {
    "follow-redirects": "^1.10.0"
  }

コードが長いので

適当に分割します

Home タブ

実行の流れ

  1. ユーザーが Home タブを開く or ボタンを押す
  2. Slack API Server が Cloud Functions に POST リクエストを投げる
  3. Cloud Functions が Slack API Server (/api/views.publish) に POST リクエストを投げる

Slack API Server からの POST を受ける

export async function post(req: any, res: any) {
  const body = req.body;
  const payload = JSON.parse(body.payload || "{}");
  
  // Home タブの構築
  if (
    body.type === "event_callback" &&
    body.event.type === "app_home_opened" &&
    body.event.tab === "home"
  ) {
    const {
      body: { playlists, userIds }
    } = JSON.parse(await getMetadata());
    await buildHome(body.event.user, playlists, userIds);
    res.status(200).send(body.challenge);
  }
}
export async function post(req: any, res: any) {
  const body = req.body;
  const payload = JSON.parse(body.payload || "{}");

  // チャンネル登録・登録解除
  else if (
    payload.type === "block_actions" &&
    (payload.actions?.[0]?.text?.text === "チャンネル登録" ||
      payload.actions?.[0]?.text?.text === "登録済み")
  ) {
    await toggleChannelSubscription(payload.user.id);
    res.status(200).send("");

    const {
      body: { playlists, userIds }
    } = JSON.parse(await getMetadata());
    await buildHome(payload.user.id, playlists, userIds);
    console.log("built.");
  }
}

Slack API Server からの POST を受ける

async function buildHome(userId: string, playlists: any[], userIds: string[]) {
  const host = "slack.com";
  const path = "/api/views.publish";
  const headers = {
    "Content-type": "application/json",
    Authorization:
      "Bearer xoxb-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  };
  const playlistsBlock = playlists.map((e: any) => {/* GAS のデータを加工 */});
  const json = {/* block を構築 */};
  const options = { host, path, method: "POST", headers };

  return new Promise((resolve, reject) => {
    const req = request(options, (res) => resolve(res.statusCode));
    req.on("error", (e) => reject(e.message));
    req.end(JSON.stringify(json));
  });
}

Slack API Server に POST を投げる

Messages タブ

実行の流れ

  1. 僕が Cloud Functions に POST リクエストを投げる
  2. Cloud Functions が Slack API Server (/api/chat.postMessage) に POST リクエストを投げる
export async function post(req: any, res: any) {
  const body = req.body;
  const payload = JSON.parse(body.payload || "{}");

  // プッシュ通知の送信
  else if (body.liveUrl) {
    const {
      body: { userIds }
    } = JSON.parse(await getMetadata());
    await pushNotification(body.liveUrl, userIds);
    res.status(200).send("");
  }
}

僕からの POST を受ける

async function pushNotification(liveUrl: string, userIds: string[]) {
  const host = "slack.com";
  const path = "/api/chat.postMessage";
  const headers = {
    "Content-type": "application/json",
    Authorization:
      "Bearer xoxb-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  };
  const options = { host, path, method: "POST", headers };
  const promises = userIds.map((u) => { /* GAS のデータを加工 */ };
    return new Promise((resolve, reject) => {
      const req = request(options, (res) => resolve(res.statusCode));
      req.on("error", (e) => reject(e.message));
      req.end(JSON.stringify(json));
    });
  });

  return Promise.all(promises);
}

Slack API Server に POST を投げる

その他

export async function post(req: any, res: any) {
  const body = req.body;
  const payload = JSON.parse(body.payload || "{}");

  // それ以外の場合は適当にレスポンスを返す
  else {
    res.status(200).send(body.challenge);
  }
}

Slack API Server に認めてもらう

懺悔

  • 本当は Slack からのリクエストが妥当か検証しないといけない
  • Bolt を使えば簡単に実現できる
  • が、Bolt は FaaS に対応していない
  • 自力で実現できなかったのでサボりました()

Block Kit Builder

  • Slack 用の UI を構築するためのツール
  • ブラウザ上で Messages・Modals・App Home Beta の UI が試せる
  • いくつかテンプレートも存在する
Block Kit Builder の App Home Preview のテンプレートで
要素同士の隙間を開けるために画像を使ってるらしい

{
  "type": "context",
  "elements": [
    {
      "type": "image",
      "image_url": "https://api.slack.com/img/blocks/bkb_template_images/placeholder.png",
      "alt_text": "placeholder"
    }
  ]
}

Block Kit Builder での空白の表現方法

開発を振り返って

  • 丸2日くらいかかった
  • 全部 GAS で良かったかも知れない
  • というか Glitch に Express + Bolt なサーバーを立てる方が多分楽だった
  • 作りたいものは一応できたので満足

まとめ

  • Slack 上でアプリが作れるので便利
    • コンテンツを表示する Home
    • ユーザーとやり取りする Messages
  • UI は Block Kit Builder で作ろう
  • 変に自前で構築せずに SDK を使おう
Made with Slides.com