先月のブログ投稿を通知する bot を Slack Platform で作った
お悩み
弊社ではブログを書くと報酬が得られるブログ手当という福利厚生があります
メンバーそれぞれ自由にブログを書いていますが、1か月に書いたブログの数に合わせて報酬が変動するため、誰が何件ブログを書いたか?を毎回確かめるのが大変という問題がありました
ということで、先月のブログを通知してくれる bot を Slack Platform で作ってみました
Slack Platform とは?
簡単に言うと今までの HTTP API ベースの処理ではなく、Trigger/Workflow/Function を定義していい感じに Slack のイベント処理ができる凄いやつです
言語は今最もホットな言語(※ワニ調べ)の Deno が採用されています
詳しくは公式サイトや、Zenn に詳しく書いてくれている方がいるので読んでみてください
完成したもの
Slack Platform さえ使えたらどの環境でも使えると思うので、似たような悩みがあればぜひ使ってみてください
MIT ライセンスなので自由に改変してもらって構いませんが、その場合は一報いただけるととても嬉しいです
通知イメージはこんな感じです
細かい実装については以下に書いていきます
フローチャート
毎月月初に Trigger
が発火して Workflow
から各種データ取得 Function
を通して記事データを取得、最終的にメッセージが Slack にポストされます
実装詳細
Trigger
Event Type
event
は scheduled
+ monthly
です
- https://api.slack.com/automation/triggers/scheduled
- https://api.slack.com/automation/triggers/scheduled#monthly-example
scheduled
スケジュールトリガーは以下の種類があります
- 一度実行(once)
- 毎時実行(hourly)
- 毎日実行(daily)
- 週次実行(weekly)
- 月次実行(monthly)
- 年次実行(yearly)
今回は「月初の月曜日に1回実行」を条件とするため以下の定義としました
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
const fetchLastMonthZennArticleStep = SendBlogPostsMessageWorkflow.addStep(
FetchZennArticles,
{},
);
SendBlogPostsMessageWorkflow.addStep(
SendMessage,
{
message: fetchLastMonthZennArticleStep.outputs.message,
},
);
const fetchLastMonthWantedlyArticleStep = SendBlogPostsMessageWorkflow.addStep(
FetchWantedlyArticles,
{},
);
SendBlogPostsMessageWorkflow.addStep(
SendMessage,
{
message: fetchLastMonthWantedlyArticleStep.outputs.message,
},
);
以下の流れで実行されます
- zenn の記事取得
function
を実行 -> メッセージ送信 - wantedly の記事取得
function
を実行 -> メッセージ送信
なんで組み込みの Schema.slack.functions.SendMessage
を使っていないの?
と思うかもしれませんが、組み込みのメッセージ送信 function
だと以下のように謎の改行が入ってしまうため、function
を実装し client.chat.postMessage
を通してメッセージ送信しています
Function
FetchZennArticles
const response = await fetch(
`https://zenn.dev/api/articles?publication_name=${publicationName}&count=20&order=latest`,
);
Zenn のブログ投稿データは publication のページにアクセスした際に通信している API から取得しています
※公開されている API かつ月一回の実行なので常識の範囲内で問題ないと思いますが、もし問題あれば連絡お願いします
FetchWantedlyArticles
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 文字列で格納されており、このデータを取得しパースすることでストーリー一覧を取得しています
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 文字列で格納されていると書きましたが、文字列の頭に //
のコメントアウトが入っているため削除してからパースしています
function extractPosts(wantedlyArticle: WantedlyArticle): Post[] {
// body の下のキーは変動するので無理やり取り出す
const articleKey = Object.keys(wantedlyArticle.body)[0] as string;
return wantedlyArticle["body"][articleKey]["posts"];
}
ざっくり以下のような階層になっているため、雑に body
の下のキーを取得してストーリーデータを抽出しています
router: {
body: {
"ランダムな値": {
posts: [ストーリーデータ]
}
}
}
その他
日付処理のライブラリ
javascript の標準日付処理は非常に癖があるため、 Deno 用の日付処理ライブラリ Ptera を使用しています
イミュータブルな日付処理や、isBefore
,isAfter
などのユーティリティクラスが非常に扱いやすくて助かりました
作者様が記事を公開しているのでぜひ読んでみてください
取得するデータの型定義
JSON を貼るだけでざっくりとした型定義を出力してくれるので、fetch
に型をつけたいぐらいの用途であればサクッと使えて便利です
以上です
clone して env に
- 送信先チャンネル ID
- zenn の publication 名
- wantedly の companyID
を設定するだけで毎月のブログ投稿を取得できるので、ぜひ使ってみてください
FB や改善希望等あればお気軽にコメントお願いします
Discussion