Discordサーバーで「もくもく読書会」というゆるやかな読書会を定期開催しています。
その中で、参加者の読書ログをまとめてDiscordに投稿するBotを作成したので、その開発メモを共有します。
GitHub Actionsを使って、自動で読書記録を収集・構造化・投稿する仕組みを構築しました。
こんな人におすすめ
- 技術コミュニティで読書会を運営している方
- Discordを活用して活動ログを残したい方
- 生成AI+自動化に興味がある方
やりたかったこと
- 毎週(あるいは毎月)、読書会で参加者が読んだ本の情報をまとめたい
- 投稿者・タイトル・感想などを、見やすい形式で整理したい
Botの主な処理内容
- Discord API から指定チャンネルの1週間分の投稿を取得
- 投稿内容を「タイトル・著者・感想」などの構造に変換(生成AIを利用)
- Markdown形式に整形し、Discordに投稿
使用技術
- Python(
discord.py
,requests
,dotenv
など) - GitHub Actions(週次で定期実行)
- Gemini API/OpenAI API(ログ要約&構造化)※ 好きな生成AIのAPIでOK
- Discord Bot(Botトークンで認証)
事前準備
- テスト用のDiscordサーバーがなければ作成します。
Discord Botの作成方法
-
Discord Developer Portal にアクセス
-
「New Application」→ Botを作成
-
「Bot」タブからTokenを取得
-
「OAuth2」→ URLを作成してBotをサーバーに招待(
Send Messages
,Read Messages History
,View Channels
権限を付与)
-
生成されるURLからDiscordチャネルへ招待
ローカルでの動作確認方法
-
.env
ファイルを作成し、以下の環境変数を設定
DISCORD_BOT_TOKEN=xxxxxxxxxxxxxxxx
DISCORD_CHANNEL_ID=123456789012345678 # 本番用チャンネル
DISCORD_CHANNEL_ID_TEST=123456789012345678 # テスト用チャンネル
GEMINI_API_KEY=sk-xxxxxxx
Discordのチャネルは本番用とテスト用で分けて管理していました。
- Discord API から対象チャンネルの1週間分の投稿を取得 ← 本番用のチャネルから読み込み
- Markdown形式に整形して、Discordに投稿 ← テスト用のチャネルに投稿
生成AIは好きなものを使ってください。今回はGEMINIを使ってみました。
- 以下の手順でスクリプトを実行
python3 fetch_messages.py # Discordから過去ログを取得 → messages.txt に保存
python3 main.py # 要約とDiscordへの投稿を実行
※.envファイル, messages.txt は.gitignoreに含めておきます。
Discordから過去ログを取得(fetch_messages.py)
コードの例です。適宜変更してください。
import os
import asyncio
import discord
from dotenv import load_dotenv
from datetime import datetime, timedelta
# .env からトークンとチャンネルIDを読み込み
load_dotenv()
TOKEN = os.getenv("DISCORD_BOT_TOKEN")
CHANNEL_ID = int(os.getenv("DISCORD_CHANNEL_ID"))
DAYS_BACK = 7 # 過去〇日分
intents = discord.Intents.default()
intents.message_content = True
client = discord.Client(intents=intents)
@client.event
async def on_ready():
print(f"Bot logged in as {client.user}")
channel = client.get_channel(CHANNEL_ID)
if not channel:
print("チャンネルが見つかりません")
await client.close()
return
# 日付フィルター:過去7日間の投稿のみ
since = datetime.utcnow() - timedelta(days=DAYS_BACK)
print(f"{DAYS_BACK}日前以降のメッセージを取得中...")
messages = []
async for msg in channel.history(after=since, limit=None, oldest_first=True):
if msg.author.bot:
continue
content = msg.content.strip()
if content:
messages.append(content)
with open("messages.txt", "w", encoding="utf-8") as f:
for m in messages:
f.write(m + "\n")
print(f"{len(messages)} 件のメッセージを messages.txt に保存しました")
await client.close()
# 実行
asyncio.run(client.start(TOKEN))
要約&投稿(main.py)
main.py
内部では、以下のようなプロンプトで投稿を構造化しています:
以下のチャットログには、参加者が読んでいる本のタイトル、著者、感想などが含まれています。このログから、各書籍ごとに以下の形式で情報をまとめてください:
---
📚 書籍タイトル:(書名を抽出)
🖋 著者:(わかれば著者名、なければ「不明」)
💬 感想:(内容や気づき・学びなど、投稿から読み取れる範囲で要約)
--- の区切りで複数冊まとめてください。
その結果、以下のような投稿になりました。
プロンプトや構造化はもう少し工夫の余地が必要かもしれません。
コード全文の例です。
import os
import asyncio
import discord
from dotenv import load_dotenv
from pathlib import Path
import traceback
import google.generativeai as genai
# --- GeminiでMarkdown抽出 ---
def extract_book_info(text: str, model) -> str:
print("LLMでMarkdown抽出:")
prompt = f"""
以下のチャットログには、参加者が読んでいる本のタイトル、著者、感想などが含まれています。このログから、各書籍ごとに以下の形式で情報をまとめてください:
---
📚 書籍タイトル:(書名を抽出)
🖋 著者:(わかれば著者名、なければ「不明」)
💬 感想:(内容や気づき・学びなど、投稿から読み取れる範囲で要約)
--- の区切りで複数冊まとめてください。
【チャットログ】
{text.strip()}
""".strip()
try:
response = model.generate_content(prompt)
return response.text.strip()
except Exception as e:
return f"[ERROR] 抽出エラー: {e}"
# --- 長文をDiscord投稿制限に合わせて分割 ---
def split_text_to_chunks(text, max_length=1990):
chunks = []
current = ""
for line in text.splitlines(keepends=True):
if len(current) + len(line) > max_length:
chunks.append(current)
current = ""
current += line
if current:
chunks.append(current)
return chunks
# --- Discord投稿処理 ---
async def run_bot(token, channel_id, model, messages_file: Path):
intents = discord.Intents.default()
intents.guilds = True
bot = discord.Client(intents=intents)
@bot.event
async def on_ready():
print(f"Logged in as {bot.user}")
channel = await bot.fetch_channel(channel_id)
print(f"投稿先チャンネル: {channel.name}")
if not messages_file.exists():
print("[ERROR] messages.txt が存在しません")
await bot.close()
return
with messages_file.open("r", encoding="utf-8") as f:
text = f.read()
if not text.strip():
print("[WARN] 有効な投稿がありません")
await bot.close()
return
print("全文から抽出を実行")
markdown = extract_book_info(text, model)
print("投稿内容(全体):\n", markdown)
# 2000文字制限に合わせて分割投稿
chunks = split_text_to_chunks(markdown, max_length=1990)
for i, chunk in enumerate(chunks):
await channel.send(chunk)
print(f"投稿 {i+1}/{len(chunks)} 成功")
await asyncio.sleep(1)
await bot.close()
await bot.start(token)
# --- メイン関数 ---
def main():
print("main.py を実行しています")
load_dotenv()
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
DISCORD_BOT_TOKEN = os.getenv("DISCORD_BOT_TOKEN")
# DISCORD_CHANNEL_ID = os.getenv("DISCORD_CHANNEL_ID_TEST")
DISCORD_CHANNEL_ID = os.getenv("DISCORD_CHANNEL_ID")
MESSAGES_FILE = Path("messages.txt")
print("環境変数確認:")
print("GEMINI_API_KEY:", "OK" if GEMINI_API_KEY else "[ERROR]未設定")
print("DISCORD_BOT_TOKEN:", "OK" if DISCORD_BOT_TOKEN else "[ERROR]未設定")
print("DISCORD_CHANNEL_ID:", DISCORD_CHANNEL_ID if DISCORD_CHANNEL_ID else "[ERROR]未設定")
if not GEMINI_API_KEY or not DISCORD_BOT_TOKEN or not DISCORD_CHANNEL_ID:
print("[ERROR] .env の環境変数が不足しています")
return
genai.configure(api_key=GEMINI_API_KEY)
model = genai.GenerativeModel(model_name="models/gemini-1.5-flash")
DISCORD_CHANNEL_ID = int(DISCORD_CHANNEL_ID)
try:
asyncio.run(run_bot(DISCORD_BOT_TOKEN, DISCORD_CHANNEL_ID, model, MESSAGES_FILE))
except Exception as e:
print("[ERROR]実行中に例外が発生しました")
traceback.print_exc()
if __name__ == "__main__":
main()
GitHub Actionsで自動実行
.github/workflows/booklog.yml
にスケジュールを記述します。
GitHub Actionsでは、UTCで指定する必要があるようです。
例えば、日曜の9時に実行するには cron: '0 0 * * 0'
と指定します。
動作確認用にworkflow_dispatch
で手動実行できるようにしておきます。
name: Weekly Discord Summary
on:
schedule:
- cron: '0 0 * * 0' # 毎週日曜 0:00 UTC(日本時間で9:00)
workflow_dispatch: # 手動実行も可能
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: リポジトリをチェックアウト
uses: actions/checkout@v3
- name: Python セットアップ
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: 依存パッケージのインストール
run: pip install -r requirements.txt
- name: fetch_messages.py 実行
env:
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }}
DISCORD_CHANNEL_ID: ${{ secrets.DISCORD_CHANNEL_ID }}
run: python fetch_messages.py
- name: main.py 実行
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }}
DISCORD_CHANNEL_ID_TEST: ${{ secrets.DISCORD_CHANNEL_ID_TEST }}
run: python main.py
環境変数(Secrets)を設定
GitHubの「Settings > Secrets」にて以下を登録:
Name | 内容 |
---|---|
DISCORD_BOT_TOKEN |
Discord Botのトークン |
DISCORD_CHANNEL_ID |
チャンネルID |
GEMINI_API_KEY |
GeminiのAPIキー |
補足:構造化ロジックは柔軟に
- AIに完全に任せると、誤変換や抜け漏れも発生します。
- 投稿フォーマットに少しルールを設けると精度が上がる可能性があるが、使いにくくなる。
- 例:「タイトル:」「感想:」などの接頭辞を統一
はまったポイント
Discordの投稿制限(2000文字)
- 対応策:セクションごとに分割/テキストの圧縮要約
Botがチャンネルに投稿できない
- 対応策:Botに「メッセージ送信」権限が付与されているか要確認
GitHub Actionsのタイムラグ
- 1時間程度のラグが発生する可能性があるが、今回の用途では問題なし。
AWS Lambdaとの比較検討(補足)
項目 | GitHub Actions | AWS Lambda + EventBridge |
---|---|---|
実行の正確性 | やや遅延あり(数分〜1時間) | 秒単位で実行可能 |
タイムゾーン設定 | UTC指定のみ | JST指定が可能 |
運用管理 | GitHubに集約できてシンプル | Lambdaデプロイ管理が必要 |
拡張性 | 限定的(GitHub上で完結) | 他のAWSリソースと連携しやすい |
スケジュール通知Botのような「時刻精度」が求められる場合は AWS Lambda の方が向いていますが、読書ログBotのような「週次集計」では GitHub Actions で十分。
まとめ
GitHub Actions、Discord API、生成AIを組み合わせることで、簡単に読書ログ集計Botを構築できました。今後は、書籍内容の抽出精度やプロンプトの改善、Slack対応なども検討していきたいです。
残課題
書籍の内容を抜き取るロジックはまだ改善の余地あり。書籍の内容と感想がミスマッチしていることがあります。プロンプトの改善だけでいいのかは要確認です。