💡

ラズパイサーバーでdiscord botを常時稼働させる

に公開

合同会社Asmiraiでは、業務委託メンバーの日報(作業ログ)をDiscordで送ってもらう運用をしています。
このログ、使い方次第では案件の稼働時間管理や、今後の見積もり精度アップに使える宝の山では???

ただ GCP や AWS に常時課金するほどでもない…

ということで埃かぶってた Raspberry Pi Zero 2 W を引っ張り出してサーバー化しました!
microSDカードリーダー なども使用します。

:::note warn
この記事では サービスアカウントの秘密鍵(JSON) を使って Google Sheets にアクセスします。
・鍵ファイルは GitHub などに絶対コミットしない
・権限は最小限(スプレッドシート編集だけ)
・必要に応じて IP 制限やローテーション
など、運用面のセキュリティは各自の判断でお願いします。
:::

概要

arch

転記されるスプシの様子

データは仮です。
sp

目的

案件ごとの稼働時間集計や見積もりのデータをいつか活用したいので、Googleスプレッドシートに蓄積する

この記事で説明すること

  • Discord Bot 作成
  • Raspberry Pi セットアップ
  • Google Sheets API 認証(サービスアカウントJSON)
  • Python + discord.py 実装
  • systemd 自動起動

手順1: Discord botの作成

  1. 下記にアクセスして New Application を押して Create します

    https://discord.com/developers/applications

  2. 適当なアイコンを設定しましょう

  3. 左の OAuth2 → URL Generator で、Scopes は bot にチェック、
    Bot Permissions は View Channels Send Messages Read Message History Add Reactions を選択します
    discordbotpermission.png

  4. 画面下で Generated URL が作成されるので、アクセスして Discord サーバーに bot を追加します。

  5. 左の Bot メニューから Message Content Intent を ON にして Save Changes を押します
    bot1


手順2: Raspberry Pi の初期設定

下記のリンクから Imager 書き込み用のソフトをインストールします
https://www.raspberrypi.com/software/

  1. Device は今回使用する Zero 2 W を指定
  2. OS は Raspberry Pi OS Lite 64bit を選択
  3. Hostname や User名、Wi-Fi 等の設定を行います。この時 SSH の設定を有効化 してください。

手順3: Raspberry PiにSSH接続

  1. ターミナルで以下のコマンドを実行します
ssh ユーザー名@ホスト名.local

ユーザー名とホスト名は先ほど設定したものに置き換えてください。

:::note warn
お使いのPCのWi-Fiは先ほど設定したものと同じものに接続します
:::


手順4: zram(スワップ)を増やす

効果があるかわかりませんが、メモリ不足クラッシュ対策に増やしておきます。

  1. 設定ディレクトリの作成
sudo mkdir -p /etc/systemd/zram-generator.conf.d
  1. override.conf の作成
sudo nano /etc/systemd/zram-generator.conf.d/override.conf
  1. 編集
[zram0]
zram-size = 1024
compression-algorithm = zstd

保存(Ctrl+O → Enter)
終了(Ctrl+X)

  1. 再起動
sudo reboot
  1. 確認
swapon --show

SIZE1024M になっていればOKです。


手順5: Google Sheets API と Google Drive API の有効化

ラズパイ側からスプシを編集するための API 有効化を行います。

  1. 下記にアクセスし、新規プロジェクトを作成
    https://console.cloud.google.com/welcome

  2. 左のメニューから 「有効なAPIとサービス」 をクリックし、
    「APIとサービスを有効にする」をクリックします
    gcp1

  3. Google Sheets API を有効化します

  4. 同様の手順で Google Drive API も有効化します


手順6: サービスアカウントと鍵ファイルの作成

6-1. サービスアカウント作成

  1. GCP コンソールで
    「IAM と管理」 → 「サービスアカウント」 を開く

  2. 「サービスアカウントを作成」から

    • 名前: raspi-worklog-bot
    • ロールは後から付けるので一旦スキップ or 最小限でOK

作成されるメールアドレスの例:

raspi-worklog-bot@discord-bot-XXXXX.iam.gserviceaccount.com

このメールアドレスは後で使うのでメモしておきます。

6-2. サービスアカウントキー(JSON)の作成

  1. 作成したサービスアカウントを選択
  2. 「鍵」タブ → 「鍵を追加」 → 「新しい鍵を作成」
  3. 種類: JSON を選んでダウンロード

discordbot-sa.json などの名前にして保管しておきます。

:::note warn
この JSON は 超重要な秘密鍵 なので、
GitHub や共有ストレージに上げない・メール添付しない・バラ撒かないよう注意してください。
:::

6-3. スプレッドシートの用意&共有

  1. Google スプレッドシートで、以下のようなカラムを持つシートを作成します(シート名は「作業ログ」など)

    sps1

  2. 右上の 共有 ボタンを押し、
    先ほどのサービスアカウントのメールアドレスを「編集者」として追加します。


手順7: ラズパイに gcloud と鍵ファイルを配置

7-1. gcloud のインストール

ラズパイに SSH で接続した状態で実行。

sudo apt-get install -y apt-transport-https ca-certificates gnupg
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" \
 | sudo tee /etc/apt/sources.list.d/google-cloud-sdk.list
sudo mkdir -p /usr/share/keyrings
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg \
 | sudo gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
sudo apt-get update
sudo apt-get install -y google-cloud-cli
gcloud version

gcloud init は今回は必須ではありませんが、プロジェクト選択などしたい場合は実行しておいてもOKです)

7-2. サービスアカウントキーをラズパイへコピー

Mac からラズパイに discordbot-sa.json をコピーする例(scp):

scp discordbot-sa.json ユーザー名@ホスト名.local:/home/ユーザー名/discord-bot/discordbot-sa.json

ラズパイ側でファイルがあることを確認:

ls /home/ユーザー名/discord-bot/discordbot-sa.json

手順8: Python 環境構築と動作確認

8-1. ディレクトリ作成 & venv

ラズパイ上で:

mkdir -p ~/discord-bot
cd ~/discord-bot
python -m venv venv
source venv/bin/activate
pip install gspread google-auth google-auth-httplib2 google-auth-oauthlib discord.py

8-2. test_sheets.py で単体テスト

nano test_sheets.py

中身:

from google.oauth2 import service_account
import gspread

SCOPES = [
    "https://www.googleapis.com/auth/spreadsheets",
    "https://www.googleapis.com/auth/drive",
]

SERVICE_ACCOUNT_FILE = "discordbot-sa.json"  # 同じディレクトリに置いた場合

def get_sheet():
    creds = service_account.Credentials.from_service_account_file(
        SERVICE_ACCOUNT_FILE,
        scopes=SCOPES,
    )
    gc = gspread.authorize(creds)
    return gc.open("作業ログ").sheet1   # あなたのスプレッドシート名に合わせて変更

def main():
    sheet = get_sheet()
    sheet.append_row(["テスト案件", "user", "接続テスト", 0.1, "SAで書き込めた"])
    print("書き込みOK")

if __name__ == "__main__":
    main()

実行:

python test_sheets.py

書き込みOK と表示され、スプレッドシートに1行追記されていれば成功です。


手順9: bot の実装

:::note warn
Discord サーバーに、チャンネルカテゴリ「受託案件」配下に案件ごとのチャンネルがある前提の実装です。
必要に応じてカテゴリ名など修正してください。
:::

9-1. bot.py の作成

cd ~/discord-bot
nano bot.py

中身(トークンは必ず置き換える&本当は環境変数推奨):

import re

import discord
from google.oauth2 import service_account
import gspread

# ==== 設定ここだけ変える ====
DISCORD_TOKEN = "YOUR_DISCORD_BOT_TOKEN"  # 実際のトークンは.envなどで管理推奨
SPREADSHEET_NAME = "作業ログ"              # スプレッドシート名
TARGET_CATEGORY_NAME = "受託案件"          # 監視するカテゴリ名
SERVICE_ACCOUNT_FILE = "discordbot-sa.json"  # サービスアカウントJSONのパス
# ==========================

SCOPES = [
    "https://www.googleapis.com/auth/spreadsheets",
    "https://www.googleapis.com/auth/drive",
]


def get_sheet():
    """サービスアカウントで Google Sheets のシートを返す"""
    creds = service_account.Credentials.from_service_account_file(
        SERVICE_ACCOUNT_FILE,
        scopes=SCOPES,
    )
    gc = gspread.authorize(creds)
    return gc.open(SPREADSHEET_NAME).sheet1


def parse_worklog(content: str, author_name: str, project_name: str):
    """
    作業ログメッセージをパースして、スプレッドシートに書き込む用の行リストを返す。

    想定フォーマット:
    [作業ログ]
    作業内容:今後の方針MTG
    稼働時間 : 0.25
    メモ:aaa

    作業内容:API設計
    稼働時間(時間): 2.5
    メモ: 楽しかった
    """

    lines = content.splitlines()

    # 先頭が [作業ログ] でなければ無視
    if not lines or not lines[0].startswith("[作業ログ]"):
        return []

    # 全角/半角コロン・スペースに緩く対応
    task_re = re.compile(r"^作業内容\s*[::]\s*(.+)$")
    hours_re = re.compile(r"^稼働時間.*[::]\s*(.+)$")  # 稼働時間(時間): も拾う
    memo_re = re.compile(r"^メモ\s*[::]\s*(.+)$")

    entries = []
    current = {}

    for line in lines[1:]:
        line = line.strip()
        if not line:
            continue

        m_task = task_re.match(line)
        if m_task:
            # もし前の作業が残っていたら確定
            if "作業内容" in current:
                entries.append(current)
                current = {}
            current["作業内容"] = m_task.group(1)
            continue

        m_hours = hours_re.match(line)
        if m_hours:
            value = m_hours.group(1)
            # 「0.25時間」みたいな書き方にも対応
            value = value.replace("時間", "").strip()
            current["稼働時間"] = value
            continue

        m_memo = memo_re.match(line)
        if m_memo:
            current["メモ"] = m_memo.group(1)
            continue

    # 最後の1件を追加
    if current.get("作業内容"):
        entries.append(current)

    rows = []
    for e in entries:
        try:
            hours = float(e.get("稼働時間", "0"))
        except ValueError:
            hours = 0.0

        rows.append([
            project_name,          # 案件名 = チャンネル名
            author_name,           # 作業者 = 投稿者名
            e.get("作業内容", ""), # 作業内容
            hours,                 # 稼働時間(時間)
            e.get("メモ", ""),     # メモ
        ])

    return rows


# ==== Discord クライアント ====
intents = discord.Intents.default()
intents.message_content = True  # Developer Portal で Message Content Intent を ON にしておく
client = discord.Client(intents=intents)


@client.event
async def on_ready():
    print(f"Logged in as {client.user.name} ({client.user.id})")


@client.event
async def on_message(message: discord.Message):
    # 自分自身には反応しない
    if message.author == client.user:
        return

    # カテゴリが「受託案件」以外のチャンネルは無視
    category = getattr(message.channel, "category", None)
    if category is None or category.name != TARGET_CATEGORY_NAME:
        return

    # 作業ログ以外は無視
    if not message.content.startswith("[作業ログ]"):
        return

    # 投稿者名(ニックネーム優先)
    author_name = message.author.display_name
    # 案件名 = チャンネル名
    project_name = message.channel.name

    # パース
    rows = parse_worklog(message.content, author_name, project_name)
    if not rows:
        await message.channel.send("作業ログの形式がパースできませんでした…")
        return

    # Sheets に書き込み
    try:
        sheet = get_sheet()
        for row in rows:
            sheet.append_row(row)

        await message.add_reaction("✅")
        await message.channel.send(
            f"{len(rows)}件の作業ログをスプレッドシートに保存しました。"
        )

    except Exception as e:
        await message.channel.send(
            f"スプレッドシート書き込みでエラーが発生しました: {e}"
        )


# 起動
client.run(DISCORD_TOKEN)

9-2. bot.py の実行テスト

python bot.py

コンソールに

Logged in as {bot名} (ID)

のように出たら、Discord サーバーの 「受託案件」カテゴリ配下のチャンネルで以下のように送信してみます。

[作業ログ]
作業内容:バグ修正1
稼働時間 : 0.25
メモ:わーい

作業内容:バグ修正2
稼働時間 : 0.25
メモ:わーい2

✅ リアクションと「2件の作業ログをスプレッドシートに保存しました。」のメッセージが返り、スプレッドシートに 2 行追加されていれば OK です。


手順10: systemd 化(自動起動)

ラズパイ起動時に自動で Bot が立ち上がるようにします。

10-1. systemd のサービスファイル作成

sudo nano /etc/systemd/system/worklog-bot.service

中身:{user名} はご自身のユーザーに変更してください。

[Unit]
Description=Discord Worklog Bot
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User={user名}
WorkingDirectory=/home/{user名}/discord-bot
ExecStart=/home/{user名}/discord-bot/venv/bin/python /home/{user名}/discord-bot/bot.py
Restart=always
RestartSec=5
Environment="PYTHONUNBUFFERED=1"

[Install]
WantedBy=multi-user.target

10-2. 設定の再読み込みと自動起動の有効化

# 設定ファイル(.service)の再読み込み
sudo systemctl daemon-reload

# worklog-bot サービスを新しい状態で再起動
sudo systemctl restart worklog-bot

# サービスの状態を確認する
sudo systemctl status worklog-bot

# ラズパイ起動時の自動起動を有効化
sudo systemctl enable worklog-bot

active (running) になっていれば成功です。

完成!!

お疲れ様でした!
以上で完成です!あとはラズパイを適当な USB ポートから電源供給しておけば、
Discord に投げられた [作業ログ] が自動で Google スプレッドシートに溜まっていく小さな「社内専用サーバー」 の出来上がりです。

剥き出しで放置されるサーバー()...
raspserver.jpg

Discussion