はじめに
この記事は ZOZO Advent Calendar 2022 カレンダーVol.6の15日目の記事です。
こんにちは、初投稿です。新卒一年目で、普段はデータ基盤の運用を行なっております。
私は普段、Notionを用いて毎日のインプットを整理しています。
その中でも後述するWeb Clipperを用いて、日々参考となったWebページを管理しています。(便利)
また会社では気になるネタ記事を共有するSlackのチャンネルがあるのですが、「あとで読も〜」と思って絵文字リアクションのみしてしまい、見逃す事案が多発しています。
Web ClipperはChromeの拡張機能なので一度リンクを開けばいいのですが、それすらも億劫なのです。
なので特定の絵文字でリアクションをすると、Web Clipperのデータベースに登録されるBotを作ります。
今回作るもの
SlackでURLが含まれるメッセージに、 絵文字でリアクションすると、
NotionのWeb Clipper用のデータベースに登録されます。
Notion Web Clipperについて
Notion Web ClipperはウェブページをNotionに保存する機能です。
こんな感じ。
それだけです。ただそれでいいのです。Notionのデータベースが優秀だから。
Notionのデータベースの検索は全てのカラム情報に全文検索が走るため、自動保存である保存したwebページのタイトルでも検索にかかるし、任意に入力したタグやキーワードに対してもヒットします。
またビューを用いて任意のフィルター条件を登録しておけば、体験の良い参照ができます。(「高頻度アクセスビュー」や「Python関連ビュー」など)
その他にもデータベースのリレーション機能使ってタグをライブラリ化している人もいたりします。
最近では、とにかくバンバンWeb Clipperに保存しています。
そしてググる前にNotion内で検索し、それでもない場合はGoolge先生に聞きます。
「お、いい感じの記事〜登録〜」と思ってポチったら、数ヶ月前に既に登録されていることとかもめっちゃあります。
余談: Save to Notionが良さげ
この記事を書く前まで、Notionの公式が出しているWeb Clipperの拡張機能を用いてましたが、調べてみるとサードパーティ製のSave to Notionというものが出ていました。
Save to Notionではページ保存時にプロパティを設定できたり、ハイライト機能があったり、使い勝手が良かったので私もこちらに乗り換えました。
Slackの準備
Slack Appの準備は主に以下です。
- Events API を用いて、絵文字リアクションがある度にGAS のURLにイベントを送信する
- Conversations API を用いて、メッセージ内容を取得するので、権限を付与する
- 必要な情報を控える
Event APIの設定
適当なSlack Appを作成し、ワークスペースにインストールします。
また今回のAppは完全にプライベート用なので、Appの配布はスキップします。
次にEvent Subsriptionsの設定です。
まず、Request URL
については後述するGASのデプロイURLなので、GAS実装後に入力します。
次に、どのイベントに対してサブスクライブする設定ですが、今回は「絵文字のリアクションが追加された時」なので、下記画像のように reaction_added
を選択します。
権限の設定
次にメッセージ内容を取得するための権限設定です。
今回は、 https://slack.com/api/conversations.replies
(リファレンス)を用いるかつ、チャンネルへの投稿メッセージを対象としていますので、
channels:history 権限を与えます。
Features >> OAuth & PermissionsのScopeより設定できます。
(reactions:read
はEvent Subsriptionsの設定で自動追加されたものです)
必要な情報
以下の情報を控えます。
- Bot User OAuth Token
- Slack api画面の Features >> OAuth & Permissionsより取得できます
- 自身のメンバーID
- この記事を参考にしました
- 自身のリアクションのみ保存する時の判別として使います
Notionの準備
Notionの準備は主に以下です。
- インテグレーションを作成する
- 対象のデータベースに対して、コネクトの追加を行う
- 必要な情報を控える
インテグレーションの作成
公式の通り進めていきます。
ブラウザからNotionにログインし、https://www.notion.so/my-integrations にアクセスします。
「新しいインテグレーションを作成する」を選択し、適当なNameを入力します。その他はデフォルト値のまま進めました。
インテグレーションを作成すると、SecretsのセクションにInternal Integration Token
が表示されるの控えます。
コネクトの追加
こちらも公式通りに。
次に対象のデータベースに対して、作成したインテグレーションをコネクト追加します。
必要な情報
- 上述した
Internal Integration Token
- データベースID
- こちらに取得方法が記載されています
GAS
やっと本題です。
こういった自動化Botを作るときはGAS (Google Apps Script) を用いるのが定番らしいので、GASで作りました。
// Slack Setting
const SLACK_APP_TOKEN = "Bot User OAuth Token入れる"
const MEMBER_ID = "自身のメンバーID入れる"
// Notion Setting
const DATABASE_ID = "データベースID入れる"
const NOTION_TOKEN = "Internal Integration Token入れる"
function doPost(e) {
// Event APIの検証時はchallengeを返す
try {
const json = JSON.parse(e.postData.getDataAsString());
if (json.type == "url_verification") {
return ContentService.createTextOutput(json.challenge);
}
}
catch (ex) {
Logger.log(ex);
}
const json = JSON.parse(e.postData.getDataAsString());
const channel = json.event.item.channel;
const ts = json.event.item.ts;
const reaction = json.event.reaction;
const memberId = json.event.user
if (reaction != "linked_paperclips" || memberId != MEMBER_ID) {
return
}
const message = getMessage(ts, channel);
const urls = extractUrlslFromMessage(message);
const permalink = getPermalink(ts, channel);
for (url of urls) {
postNotionDB(url.title, url.url, permalink)
}
const output = ContentService.createTextOutput(JSON.stringify({ result: "Ok" }));
output.setMimeType(ContentService.MimeType.JSON);
return output;
}
function getMessage(ts, channel) {
const url = "https://slack.com/api/conversations.replies";
const options = {
"method": "get",
"contentType": "application/x-www-form-urlencoded",
"payload": {
"token": SLACK_APP_TOKEN,
"channel": channel,
"ts": ts
}
};
const response = UrlFetchApp.fetch(url, options);
const json = JSON.parse(response);
// スレッド返信は考慮しないので最初のmessageのみ
const message = json.messages[0]
return message
}
function getPermalink(ts, channel) {
const url = "https://slack.com/api/chat.getPermalink"
const options = {
"method": "get",
"contentType": "application/x-www-form-urlencoded",
"payload": {
"token": SLACK_APP_TOKEN,
"channel": channel,
"message_ts": ts
}
};
const response = UrlFetchApp.fetch(url, options);
const json = JSON.parse(response);
const permalink = json.permalink
return permalink
}
function extractUrlslFromMessage(message) {
const urls = []
// attachments
if ("attachments" in message) {
for (attachment of message.attachments) {
urls.push({
title: attachment.title,
url: attachment.original_url
})
}
}
// blockのelements見る。attachmentsにあればskipする
for (element of message.blocks[0].elements[0].elements) {
if (element.type != "link") {
continue
}
if (urls.some(url => url.url != element.url) || urls.length == 0) {
urls.push({
title: element.url,
url: element.url
})
}
}
return urls
}
function postNotionDB(title, url, permalink) {
const post_url = 'https://api.notion.com/v1/pages';
const json = {
parent: {
database_id: DATABASE_ID,
},
properties: {
"Name": {
"title": [{
"text": {
"content": title
}
}]
},
"URL": {
"url": url
},
"SlackURL": {
"url": permalink
}
}
}
const options = {
"method": "POST",
"headers": {
"Content-type": "application/json",
"Authorization": "Bearer " + NOTION_TOKEN,
"Notion-Version": '2022-06-28',
},
"payload": JSON.stringify(json),
};
UrlFetchApp.fetch(post_url, options);
}
とくに難しいことはしていないので詳しい解説は省略します。
若干、工夫した点は絵文字リアクションを通してWeb Clipperのデータベースに登録するときは、
SlackURL
というカラムに、該当スレのパーマリンクも一緒に登録します。
スレッド内で議論などがあった場合、Notionからそのパーマリンクを参照することで、あとで見返すときに便利そうだなと思いつけました。
注意点と修正予定箇所
勢いで作ったため、いくつか注意点があります。
まず、Slack上でのWebページのプレビューがない場合は、タイトルがURLになります。
上記コードの attachments
がプレビューの情報を含まれるのですが、これがない場合はURLの文字列の情報しか取得できなかったので、タイトルをURLにしました。
またプレビューがない場合はblocksの値を取得して、URLを取得しています。
しかし、この中身はメッセージの書式によって再帰的な構造をもつ仕様になっており、現在のコードだとリストなどの書式が入ると上手く取得できません。
こちら (多分)修正予定です。(レスポンス仕様のリファレンスが探しても無いのだが、どこにあるのやら、、、)
さいごに
今回は初投稿で本来、データエンジニアリング業務の小ネタを書こうと思ってたのですが、
あんまりネタが無かったのと、今回作ったBotは数ヶ月前から構想はあったので、勢いでこの題材にしました。
次回からはそっちのネタ書けるといいなぁ〜