0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

読書ログまとめ投稿をする読書会用のDiscord botを作った時のメモ

Posted at

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の作成方法

  1. Discord Developer Portal にアクセス

  2. 「New Application」→ Botを作成

  3. 「Bot」タブからTokenを取得

  4. 「OAuth2」→ URLを作成してBotをサーバーに招待(Send Messages,Read Messages History, View Channels 権限を付与)
    image.png
    image.png

  5. 生成されるURLからDiscordチャネルへ招待

ローカルでの動作確認方法

  1. .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を使ってみました。

  1. 以下の手順でスクリプトを実行
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 内部では、以下のようなプロンプトで投稿を構造化しています:

以下のチャットログには、参加者が読んでいる本のタイトル、著者、感想などが含まれています。このログから、各書籍ごとに以下の形式で情報をまとめてください:
---
📚 書籍タイトル:(書名を抽出)
🖋 著者:(わかれば著者名、なければ「不明」)
💬 感想:(内容や気づき・学びなど、投稿から読み取れる範囲で要約)
--- の区切りで複数冊まとめてください。

その結果、以下のような投稿になりました。
プロンプトや構造化はもう少し工夫の余地が必要かもしれません。

image.png

コード全文の例です。

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対応なども検討していきたいです。

残課題

書籍の内容を抜き取るロジックはまだ改善の余地あり。書籍の内容と感想がミスマッチしていることがあります。プロンプトの改善だけでいいのかは要確認です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?