Skip to content

Commit c339410

Browse files
committed
update to cloudflare d1
1 parent d0b6448 commit c339410

5 files changed

Lines changed: 185 additions & 56 deletions

File tree

.env.default

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
DISCORD_BOT_TOKEN=
22
DISCORD_APPLICATION_ID=
3-
DISCORD_CHANNEL_ID=841990187047583764
3+
DISCORD_CHANNEL_ID=841990187047583764
4+
DISCORD_GUILD_ID=
5+
6+
# Cloudflare D1 Configuration
7+
D1_ACCOUNT_ID=442d745cd7f0db10e98539fe02f78e15
8+
D1_DATABASE_ID=b7a27084-2e40-448d-b81c-a12f97aa8b50
9+
D1_API_TOKEN=lNHYE49_-eNkjFaC58jO65FHVCt9LyVqp6WTiX81

CLAUDE.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
**thuuwa_tuuchi_bot** is a Discord bot that monitors voice channel activity and sends notifications when users start or end calls. The bot is written in Python using discord.py v2.3.2 and is designed to run in Docker containers.
8+
9+
## Development Commands
10+
11+
### Running the Bot Locally
12+
```bash
13+
# Install dependencies
14+
pip install -r requirements.txt
15+
16+
# Run directly (requires environment variables)
17+
python app.py
18+
19+
# Run with Docker Compose
20+
docker-compose up --build
21+
```
22+
23+
### Required Environment Variables
24+
The following environment variables must be set (see `.env.default` for template):
25+
- `DISCORD_BOT_TOKEN` - Discord bot authentication token
26+
- `DISCORD_APPLICATION_ID` - Discord application ID
27+
- `DISCORD_CHANNEL_ID` - Default channel ID for notifications
28+
- `DISCORD_GUILD_ID` - Discord guild (server) ID
29+
30+
## Architecture
31+
32+
### Core Components
33+
34+
**app.py** - Main bot client and event handlers
35+
- `MyClient` class extends `discord.Client` with custom voice state monitoring
36+
- `on_ready()` - Initializes database and syncs voice channel settings on bot startup
37+
- `on_voice_state_update()` - Monitors voice channel join/leave events
38+
- `start_call()` - Detects when a user starts a new call (first person joins empty channel)
39+
- `end_call()` - Detects when a call ends (last person leaves channel)
40+
41+
**mycommands.py** - Discord slash commands (`/callnotion` command group)
42+
- `set` - Configure notification target channel
43+
- `textchange` - Change notification message text
44+
- `changenotificationmode` - Toggle end-call notifications on/off
45+
- `offchannel` - Enable/disable notifications per voice channel (interactive UI)
46+
- `offlist` - View on/off status of all voice channels
47+
- `reset` - Reset configuration to defaults
48+
- `getnotiontext` - View current notification text
49+
50+
**bot_config.py** - Configuration loader
51+
- Loads required environment variables for Discord API
52+
- Defines bot intents (requires `discord.Intents.all()`)
53+
54+
**params.py** - Runtime state management
55+
- Loads persistent settings from database on startup
56+
- Stores in-memory state: `channel_id`, `notitext`, `is_target_channel`, `e_time`
57+
- Falls back to initial values if database load fails
58+
59+
**db_utils.py** - SQLite database operations
60+
- Database path: `db/bot_data.db`
61+
- Tables:
62+
- `notitext` - Stores notification message text
63+
- `is_target_channel` - Stores per-channel notification enable/disable status
64+
- `init_db()` - Creates tables if they don't exist
65+
- CRUD functions for notification text and channel targeting settings
66+
67+
### State Flow
68+
69+
1. Bot starts → `init_db()` creates database schema
70+
2. `params.py` loads persistent settings from database
71+
3. `on_ready()` syncs voice channels with database (adds new channels as enabled by default)
72+
4. Voice state changes trigger `on_voice_state_update()``start_call()` or `end_call()`
73+
5. Slash commands update both in-memory state (`params.py`) and database (`db_utils.py`)
74+
75+
### Call Detection Logic
76+
77+
**Start Call Detection** (app.py:44-86):
78+
- User must move to a different channel (not already in it)
79+
- Target channel must be enabled for notifications (`is_target_channel[channel_id] == True`)
80+
- Channel must have exactly 1 member (the person who just joined)
81+
- Sends embed with channel name, starter's name, timestamp, and user avatar
82+
83+
**End Call Detection** (app.py:107-136):
84+
- Only runs if `is_call_end_notification_enabled == True`
85+
- User must leave from a tracked channel
86+
- Channel must now have 0 members
87+
- Sends embed with channel name and elapsed call duration (formatted as HH:MM:SS)
88+
89+
### Deployment
90+
91+
The bot is containerized with multi-stage Docker build:
92+
- CI/CD via GitHub Actions (`.github/workflows/image-build.yml`)
93+
- Builds on push to `main` branch
94+
- Multi-architecture support (amd64, arm64)
95+
- Published to GitHub Container Registry: `ghcr.io/102ch/thuuwa-tuuchi-bot`

db_utils.py

Lines changed: 78 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,93 @@
1-
import sqlite3
1+
import os
2+
import requests
3+
from typing import Optional, Dict, List
24

3-
DB_PATH = "db/bot_data.db"
5+
# D1設定(環境変数から読み込み)
6+
D1_ACCOUNT_ID = os.environ.get("D1_ACCOUNT_ID")
7+
D1_DATABASE_ID = os.environ.get("D1_DATABASE_ID")
8+
D1_API_TOKEN = os.environ.get("D1_API_TOKEN")
9+
10+
if not all([D1_ACCOUNT_ID, D1_DATABASE_ID, D1_API_TOKEN]):
11+
raise ValueError("Missing D1 environment variables: D1_ACCOUNT_ID, D1_DATABASE_ID, D1_API_TOKEN")
12+
13+
D1_API_URL = f"https://api.cloudflare.com/client/v4/accounts/{D1_ACCOUNT_ID}/d1/database/{D1_DATABASE_ID}/query"
14+
15+
16+
def execute_d1_query(sql: str, params: Optional[List] = None) -> dict:
17+
"""D1データベースでクエリを実行"""
18+
headers = {
19+
"Authorization": f"Bearer {D1_API_TOKEN}",
20+
"Content-Type": "application/json"
21+
}
22+
23+
payload = {"sql": sql}
24+
if params:
25+
payload["params"] = params
26+
27+
try:
28+
response = requests.post(D1_API_URL, headers=headers, json=payload, timeout=10)
29+
response.raise_for_status()
30+
data = response.json()
31+
32+
if not data.get("success"):
33+
raise ValueError(f"D1 query failed: {data.get('errors', [])}")
34+
35+
return data["result"][0] if data.get("result") else {}
36+
37+
except requests.exceptions.RequestException as e:
38+
print(f"D1 API request failed: {e}")
39+
raise
440

541

642
def init_db():
7-
conn = sqlite3.connect(DB_PATH)
8-
cursor = conn.cursor()
9-
# notitext テーブル
10-
cursor.execute(
11-
"""
43+
"""D1データベーススキーマを初期化"""
44+
execute_d1_query("""
1245
CREATE TABLE IF NOT EXISTS notitext (
1346
id INTEGER PRIMARY KEY AUTOINCREMENT,
1447
text TEXT NOT NULL
1548
)
16-
"""
17-
)
18-
# is_target_channel テーブル
19-
cursor.execute(
20-
"""
49+
""")
50+
51+
execute_d1_query("""
2152
CREATE TABLE IF NOT EXISTS is_target_channel (
2253
channel_id INTEGER PRIMARY KEY,
2354
is_target BOOLEAN NOT NULL
2455
)
25-
"""
26-
)
27-
conn.commit()
28-
conn.close()
29-
30-
31-
def save_notitext(text):
32-
conn = sqlite3.connect(DB_PATH)
33-
cursor = conn.cursor()
34-
cursor.execute("DELETE FROM notitext") # 古い値を削除
35-
cursor.execute("INSERT INTO notitext (text) VALUES (?)", (text,))
36-
conn.commit()
37-
conn.close()
38-
39-
40-
def load_notitext():
41-
conn = sqlite3.connect(DB_PATH)
42-
cursor = conn.cursor()
43-
cursor.execute("SELECT text FROM notitext LIMIT 1")
44-
row = cursor.fetchone()
45-
conn.close()
46-
return row[0] if row else "@everyone" # デフォルト値
47-
48-
49-
def save_is_target_channel(channel_id, is_target):
50-
conn = sqlite3.connect(DB_PATH)
51-
cursor = conn.cursor()
52-
cursor.execute(
53-
"""
54-
INSERT OR REPLACE INTO is_target_channel (channel_id, is_target)
55-
VALUES (?, ?)
56-
""",
57-
(channel_id, is_target),
56+
""")
57+
print("D1 database initialized")
58+
59+
60+
def save_notitext(text: str):
61+
"""通知テキストをD1に保存"""
62+
execute_d1_query("DELETE FROM notitext")
63+
execute_d1_query("INSERT INTO notitext (text) VALUES (?)", [text])
64+
65+
66+
def load_notitext() -> str:
67+
"""通知テキストをD1から読み込み"""
68+
result = execute_d1_query("SELECT text FROM notitext LIMIT 1")
69+
results = result.get("results", [])
70+
71+
if results and len(results) > 0:
72+
return results[0]["text"]
73+
74+
return "@everyone"
75+
76+
77+
def save_is_target_channel(channel_id: int, is_target: bool):
78+
"""チャンネル設定をD1に保存"""
79+
execute_d1_query(
80+
"INSERT OR REPLACE INTO is_target_channel (channel_id, is_target) VALUES (?, ?)",
81+
[channel_id, 1 if is_target else 0]
5882
)
59-
conn.commit()
60-
conn.close()
6183

6284

63-
def load_is_target_channels():
64-
conn = sqlite3.connect(DB_PATH)
65-
cursor = conn.cursor()
66-
cursor.execute("SELECT channel_id, is_target FROM is_target_channel")
67-
rows = cursor.fetchall()
68-
conn.close()
69-
return {row[0]: bool(row[1]) for row in rows}
85+
def load_is_target_channels() -> Dict[int, bool]:
86+
"""全チャンネル設定をD1から読み込み"""
87+
result = execute_d1_query("SELECT channel_id, is_target FROM is_target_channel")
88+
results = result.get("results", [])
89+
90+
return {
91+
int(row["channel_id"]): bool(row["is_target"])
92+
for row in results
93+
}

docker-compose.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ services:
99
- DISCORD_APPLICATION_ID=${DISCORD_APPLICATION_ID}
1010
- DISCORD_CHANNEL_ID=${DISCORD_CHANNEL_ID}
1111
- DISCORD_GUILD_ID=${DISCORD_GUILD_ID}
12+
- D1_ACCOUNT_ID=${D1_ACCOUNT_ID}
13+
- D1_DATABASE_ID=${D1_DATABASE_ID}
14+
- D1_API_TOKEN=${D1_API_TOKEN}

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
discord.py[voice]==2.3.2
2-
pytz==2023.3
2+
pytz==2023.3
3+
requests==2.31.0

0 commit comments

Comments
 (0)