👾

先月のブログ投稿を通知する bot を Slack Platform で作った

2023/08/14に公開

お悩み

弊社ではブログを書くと報酬が得られるブログ手当という福利厚生があります
メンバーそれぞれ自由にブログを書いていますが、1か月に書いたブログの数に合わせて報酬が変動するため、誰が何件ブログを書いたか?を毎回確かめるのが大変という問題がありました
ということで、先月のブログを通知してくれる bot を Slack Platform で作ってみました

Slack Platform とは?

https://api.slack.com/start/overview
(登場当初は Next-gen Slack Platform とか呼ばれていた気がします)
簡単に言うと今までの HTTP API ベースの処理ではなく、Trigger/Workflow/Function を定義していい感じに Slack のイベント処理ができる凄いやつです
言語は今最もホットな言語(※ワニ調べ)の Deno が採用されています
詳しくは公式サイトや、Zenn に詳しく書いてくれている方がいるので読んでみてください

完成したもの

https://github.com/sugawani/slack-notify-blog-posts
使い方は README を読んでください
Slack Platform さえ使えたらどの環境でも使えると思うので、似たような悩みがあればぜひ使ってみてください
MIT ライセンスなので自由に改変してもらって構いませんが、その場合は一報いただけるととても嬉しいです
通知イメージはこんな感じです
notify-image

細かい実装については以下に書いていきます

フローチャート

flowchart
毎月月初に Trigger が発火して Workflow から各種データ取得 Function を通して記事データを取得、最終的にメッセージが Slack にポストされます

実装詳細

Trigger

Event Type

eventscheduled + monthly です

scheduled

スケジュールトリガーは以下の種類があります

  • 一度実行(once)
  • 毎時実行(hourly)
  • 毎日実行(daily)
  • 週次実行(weekly)
  • 月次実行(monthly)
  • 年次実行(yearly)

今回は「月初の月曜日に1回実行」を条件とするため以下の定義としました

schedule_trigger
type: TriggerTypes.Scheduled,
schedule: {
  start_time: datetime().add({ second: 30 }).toISO(), // trigger 作成から30秒後に起動
  frequency: {
    type: "monthly", // 月次実行
    on_days: ["Monday"], // 月曜に実行
    on_week_num: 1, // 月の第一週に実行
  },
  timezone: "JST", // タイムゾーンをJSTに設定
},

frequency の定義はスケジュールタイプによって柔軟な定義ができるので、他のタイプを使用する場合はドキュメントを参考に設定してみてください

Workflow

workflow
const fetchLastMonthZennArticleStep = SendBlogPostsMessageWorkflow.addStep(
  FetchZennArticles,
  {},
);

SendBlogPostsMessageWorkflow.addStep(
  SendMessage,
  {
    message: fetchLastMonthZennArticleStep.outputs.message,
  },
);

const fetchLastMonthWantedlyArticleStep = SendBlogPostsMessageWorkflow.addStep(
  FetchWantedlyArticles,
  {},
);

SendBlogPostsMessageWorkflow.addStep(
  SendMessage,
  {
    message: fetchLastMonthWantedlyArticleStep.outputs.message,
  },
);

以下の流れで実行されます

  1. zenn の記事取得 function を実行 -> メッセージ送信
  2. wantedly の記事取得 function を実行 -> メッセージ送信

なんで組み込みの Schema.slack.functions.SendMessage を使っていないの?
と思うかもしれませんが、組み込みのメッセージ送信 function だと以下のように謎の改行が入ってしまうため、function を実装し client.chat.postMessage を通してメッセージ送信しています
notify-newline-image

Function

FetchZennArticles

fetch_zenn_article
const response = await fetch(
  `https://zenn.dev/api/articles?publication_name=${publicationName}&count=20&order=latest`,
);

Zenn のブログ投稿データは publication のページにアクセスした際に通信している API から取得しています
※公開されている API かつ月一回の実行なので常識の範囲内で問題ないと思いますが、もし問題あれば連絡お願いします

FetchWantedlyArticles

fetch_wantedly_article
const response = await fetch(
  `https://www.wantedly.com/companies/${companyID}/stories`,
);

const wantedlyArticle = parseHTML(await response.text());
const posts = extractPosts(wantedlyArticle);

Wantedly は SSR で実装されており、Zenn のように API からデータを取得できないためスクレイピングで取得しています
※月1回の実行で通信回数も1回なので常識の範囲内で問題ないと思いますが、もし問題あれば連絡お願いします

データ取得対象のページは企業のストーリー一覧のページです
このページには <script data-placeholder-key="wtd-ssr-placeholder"> という要素に SSR 用のデータ?が JSON 文字列で格納されており、このデータを取得しパースすることでストーリー一覧を取得しています

parse_html
function parseHTML(HTMLText: string): WantedlyArticle {
  const doc = new DOMParser().parseFromString(
    HTMLText,
    "text/html",
  );
  if (!doc) {
    throw new Error("Failed to parse html");
  }

  const plaseHolderData = doc.querySelector("[data-placeholder-key]");
  if (plaseHolderData == null) {
    throw new Error("Failed to fetch router string");
  }

  // 頭に // が入っていてパースできないので取り除く
  const router = plaseHolderData.textContent.replace("// ", "");
  return JSON.parse(router) as WantedlyArticle;
}

DOM の処理には deno-dom を使用しています
JSON 文字列で格納されていると書きましたが、文字列の頭に // のコメントアウトが入っているため削除してからパースしています

extract
function extractPosts(wantedlyArticle: WantedlyArticle): Post[] {
  // body の下のキーは変動するので無理やり取り出す
  const articleKey = Object.keys(wantedlyArticle.body)[0] as string;
  return wantedlyArticle["body"][articleKey]["posts"];
}

ざっくり以下のような階層になっているため、雑に body の下のキーを取得してストーリーデータを抽出しています

type
router: {
    body: {
        "ランダムな値": {
	    posts: [ストーリーデータ]
	}
    }
}

その他

日付処理のライブラリ

javascript の標準日付処理は非常に癖があるため、 Deno 用の日付処理ライブラリ Ptera を使用しています
イミュータブルな日付処理や、isBefore,isAfter などのユーティリティクラスが非常に扱いやすくて助かりました
作者様が記事を公開しているのでぜひ読んでみてください
https://zenn.dev/tak_iwamoto/articles/8b32b27bd577b1

取得するデータの型定義

https://app.quicktype.io/
Zenn/Wantedly ともに取得できるデータに型定義は無いので、こちらのサービスを使って JSON から型定義を生成しました
JSON を貼るだけでざっくりとした型定義を出力してくれるので、fetch に型をつけたいぐらいの用途であればサクッと使えて便利です

以上です
clone して env に

  • 送信先チャンネル ID
  • zenn の publication 名
  • wantedly の companyID

を設定するだけで毎月のブログ投稿を取得できるので、ぜひ使ってみてください
FB や改善希望等あればお気軽にコメントお願いします

EGSTOCK,Inc.

Discussion