🧠 概要:
この記事は、Slackで特定のスタンプを押すと、Geminiがその記事を要約して投稿するSlackボットの作り方を説明しています。情報収集の効率化を図るため、Slackの新着記事を簡単に把握できるようにするツールです。GAS(Google Apps Script)とGeminiのAPIを利用することで、費用をかけずに実現できます。
概要
- Slackで特定のスタンプを押すと記事を要約するボットを作成。
- 情報収集の効率化を目的。
- GASとGemini APIを使用。
要約の箇条書き
-
Slackアプリの作成
- Slack APIサイトで新しいアプリを作成。
- 必要なOAuthスコープ(reactions:read, chat:write, channels:history)を追加。
-
Gemini APIキーの取得
- Gemini APIを利用するためのキーを取得。
-
Google Apps Script (GAS)の作成
- 投稿内容からURL取得。
- 記事本文を取得し、要約を生成する機能を実装。
-
イベント処理
- スタンプが押された際にSlackからのイベントを受け取る。
- 重複処理を行い、要約を再作成しないようにする。
-
要約処理
- Gemini APIを用いて記事の要約を生成。
- Slackに視認性の高い形式で投稿。
-
デプロイと設定
- GASをウェブアプリとしてデプロイ。
- Slackでのイベントサブスクリプションを設定。
-
動作確認
- Appを特定のチャンネルに追加し、動作をテスト。
- 注意点
- コードはシンプルで、エラー処理は含まれていない。改善の余地あり。
気になる記事リンクに特定のスタンプを押すとGeminiが要約して投稿してくれるSlackボットの作り方を紹介します。
SlackのRSSアプリから大量に流れる新着記事の内容を把握することは一苦労です。そこで気になる記事だけAIでサッと要約することで情報収集を効率化します。いちいちリンクをコピーしてChatGPTを開いてプロンプトを入力する…といった手間を全て省くことができます。
GAS (Google Apps Script) とGoogle AI Studioから利用できるGeminiのAPIを用いることで、簡単かつ費用をかけずにボットを運用します。
動作イメージ
Slackでスタンプを押すと…
要約が投稿される
ボットが動作する流れ
-
Slackの投稿にスタンプが押されると、イベントがGASのウェブアプリに通知される
-
GAS上で、スタンプが押された投稿内のURLを取得する
-
URLにアクセスし記事本文を取得する
-
Gemini APIを用いて本文を要約する
-
Slack Appを用いて要約をSlackに通知する
構築手順
-
Slackアプリを作成する
-
Gemini APIキーを取得する
-
Google Apps Script (GAS) を書く
-
GASをデプロイする
1. Slackアプリの作成と設定
まずはSlack APIサイトにアクセスし、新しいSlackアプリを作成します。
-
https://api.slack.com/apps にアクセスし、Create New Appをクリックする。
-
From an app manifestを選択し、Nextをクリックする。
-
ボットを導入したいSlackワークスペースを選択し、Nextをクリックする。
-
サイドバーのBasic InformationでApp nameやShort descriptionを記入する。
-
サイトバーのOAuth & Permissions」で、ScopesのBot Token Scopesの欄にあるAdd an OAuth Scopeをクリックし、reactions:read, chat:write, channels:historyを追加する。
-
Install Appの欄で、Appをワークスペースにインストールする。
-
OAuth & PermissionsからBot User OAuth Tokenを取得しメモしておく。
2. Gemini APIキーを取得する
3. Google Apps Scriptを書く
3.1 GASを作成する
-
Googleドライブにアクセス
-
左上の「+新規」をクリック
-
「その他」の先にある「Google Apps Script」をクリック
-
以降はこのプロジェクトにコードを作成していきます
3.2 スクリプト プロパティを登録する
-
サイドバーの歯車⚙️をクリックする
-
“スクリプトプロパティ”を編集をクリックし、先ほど取得したBot User OAuth TokenとAPIキーを登録するる。
プロパティの名前は「SLACK_BOT_TOKEN」「apiKey」にしておきます。 -
GAS上でスクリプトプロパティを読み取るには、以下のコードを記入します。
const SLACK_BOT_TOKEN = PropertiesService.getScriptProperties().getProperty("SLACK_BOT_TOKEN");const apiKey = PropertiesService.getScriptProperties().getProperty("apiKey");
3.3 doPost関数を作成する
この関数がSlackからのイベント (スタンプが押されたこと) を受け取ります。この関数内では、主に3つの機能を実装しています。
-
Slackからの初回URL登録時の検証リクエストへの応答
-
Slackからのリトライによる重複処理の防止
-
記事の要約の投稿
function doPost(e) { try { const postDataString = e.postData.contents; const eventData = JSON.parse(postDataString); if (eventData.type === 'url_verification' && eventData.challenge) { return ContentService.createTextOutput(eventData.challenge); } if (eventData.type === 'event_callback') { const event = eventData.event; const eventId = eventData.event_id; const cache = CacheService.getScriptCache(); const processed = cache.get(eventId); if (processed) { Logger.log(`Event ${eventId} already processed. Skipping.`); return ContentService.createTextOutput(JSON.stringify({ ok: true, message: "Already processed" })).setMimeType(ContentService.MimeType.JSON); } cache.put(eventId, 'processed', 300); if (event.type === "reaction_added" && event.reaction === "eyes") { const channelId = event.item.channel; const messageTs = event.item.ts; const url = getUrlInPost(channelId, messageTs); let title_text = getArticleText(url); let title = title_text[0]; text = title_text[1]; let summary = summarizeText(text); summary = textProcessing(summary); let postMessage = `<${url}|${title}>nn${summary}`; sendToSlack(channelId, postMessage); } } } catch (error) { Logger.log('Error: ' + error.toString() + 'n' + (e && e.postData ? e.postData.contents : 'No post data')); } return ContentService.createTextOutput(JSON.stringify({ ok: true })).setMimeType(ContentService.MimeType.JSON);}
3.4 スタンプが押された投稿からURLを取得する
Slackから送られるイベント (reaction_addedイベント ※後述) から投稿の中身を直接取得することは出来ません。そこでチャンネルIDとタイムスタンプを手掛かりに、スタンプが押された投稿を取得します。
function getUrlInPost(channelId, messageTs) { const payload = { channel: channelId, latest: messageTs, limit: 1, inclusive: true }; const options = { method: "get", contentType: "application/x-www-form-urlencoded", payload: payload, headers: { "Authorization": "Bearer " + SLACK_BOT_TOKEN }, muteHttpExceptions: true }; const response = UrlFetchApp.fetch("https://slack.com/api/conversations.history", options); const responseText = response.getContentText(); const jsonResponse = JSON.parse(responseText); const message = jsonResponse.messages[0]; let url = message.blocks[0].elements[0].elements[0].url if (url) { } else { url = message.blocks[0].elements[0].elements[1].url; } return url;}
3.5 URLにアクセスし記事本文を取得する
記事のURLからHTMLを取得し、本文を抽出する関数です。HTMLの解析にはCheerioライブラリを用います。GASの左側にある「ライブラリ」をクリックしスクリプトIDに以下を入力してください。
「1ReeQ6WO8kKNxoaA_O0XEQ589cIrRvEBA9qcWpNqdOP17i47u6N9M5Xh0」
function getArticleText(url) { const response = UrlFetchApp.fetch(url, { muteHttpExceptions: true }); let html = response.getContentText(); let $ = Cheerio.load(html); let text = $("body").text().trim(); text = text.replace(/<[^>]*>/g, '').replace(/[rn]+/g, 'n').replace(/ +/g, ' ').trim(); const maxLength = 20000; if (text.length > maxLength) { text = text.substring(0, maxLength); } return [$("title").text().trim(), text]; }
3.6 Gemini APIで記事を要約する
ここではgemini-2.0-flashを用いています。
またプロンプトも多少練ったものを記載していますが、適宜好みに合わせて修正してください。
function summarizeText(text) { const endpoint = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`; const payload = { contents: [{ parts: [{ text: `# 指示以下記事の核心を的確に捉え、読者が元記事を読まなくても主要な内容を迅速かつ正確に理解できるような端的な要約を作成してください。 ### 形式- 1つ以上3つ以下の箇条書きポイントで構成してください。(重要ではない情報の記述は不要です。)- 各箇条書きポイントの間は、Slackでの視認性を高めるため、必ず1行空けてください。- 各箇条書きポイントの先頭に、その内容と最も関連性の高いSlack標準絵文字(例: :bulb:、:chart_with_upwards_trend:、:rocket:、:mag: など)を1つだけ付けてください。- 文末は、専門的かつ簡潔な「~だ。」「~である。」調で統一してください。- Slackのmrkdwn記法を適切に使用し、重要なキーワードやフレーズがあれば *太字* で強調してください(ただし、リンクの生成は不要です)。 ### その他留意事項- 記事に含まれる主要な論点、重要な事実、データ、背景、提起されている問題、提案、結論を過不足なく含めてください。- 記事中の具体的な事例、データ、引用など、根拠のある情報を優先的に取り上げてくだ<さい。- 冗長な表現、不要な修飾語、繰り返しを徹底的に排除してください。 # 記事本文${text} # 要約` }] }], generationConfig: { maxOutputTokens: 500, temperature: 0.5, topP: 0.95, topK: 40, }, }; const options = { 'method': 'post', 'contentType': 'application/json', 'payload': JSON.stringify(payload), 'muteHttpExceptions': true }; const response = UrlFetchApp.fetch(endpoint, options); const jsonResponse = JSON.parse(response.getContentText()); const summary = jsonResponse.candidates[0].content.parts[0].text; return summary;}
また、Geminiが生成したテキストを処理する関数も以下に定義します。GASからSlackに投稿する際はアスタリスクによる太字処理がうまくかからないことが多いため、アスタリスクの前後に半角スペースを加える後処理をすることで、確実に太字で表示されるようにしています。
function textProcessing(text) { text = text.replace(/<br>/g, "").replace(/n{3,}/g, "nn"); let asteriskCount = 0; let result = ''; for (let i = 0; i < text.length; i++) { const char = text[i]; if (char === '*') { asteriskCount++; if (asteriskCount % 2 === 0) { result += char + ' '; } else { result += ' ' + char; } } else { result += char; } } return result}
3.7 Slackに投稿する
スタンプが押した投稿のあるチャンネルと同じチャンネルに要約を投稿します。異なるチャンネルに投稿したい場合は、channel: にチャンネル名を指定してください。
function sendToSlack(channelId, message) { const url = 'https://slack.com/api/chat.postMessage'; const payload = { channel: channelId, text: message }; const options = { method: 'post', contentType: 'application/json', headers: { Authorization: 'Bearer ' + SLACK_BOT_TOKEN }, payload: JSON.stringify(payload) }; UrlFetchApp.fetch(url, options);}
4. GASをデプロイする
4.1 GASをデプロイする
コードが作成できたら、このGASをSlackからのリクエストを受け取れる「ウェブアプリ」として公開します。
-
GASエディタ上部の「デプロイ」→「新しいデプロイ」を選択する
-
「種類の選択」で「ウェブアプリ」を選択する
-
以下のように設定
-
実行するユーザー: 「自分」
-
アクセスできるユーザー: 「全員」
-
-
「デプロイ」ボタンをクリックします。
-
初回デプロイ時には、Googleアカウントの認証を求められるため、許可を与えて進めてください。
-
デプロイが完了すると、「ウェブアプリのURL」が表示されるため、このURLをコピーしてください
4.2 Slack AppのEvent Subscriptionsを登録する
-
Slack Appの設定に戻り、サイドバーからEvent Subscriptionsに移動する
-
Enable Eventsをオンにし、Request URLの欄に、GASをデプロイした際にコピーしたURLを入力する。
検証が成功すると「Verified」と表示される。 -
Subscribe to bot eventsの欄でAdd bot User Eventをクリックし、「reaction_added」を追加する
-
ページ下部のSave Changesをクリックする
4.3 Slack Appをチャンネルに加える
Slackアプリを開き、要約したい投稿があるチャンネルに作成したSlack Appを加えます。
「/invite @SlackAppの名前」と打つことで招待できます。
これで全ての設定が完了です。
終わりに
今回紹介したコードは、エラー処理や例外処理などを全くしていないシンプルなものになります。このコードをスタートラインにして、ぜひ高度化に挑戦してみてください。
Views: 2