Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,9 @@ cython_debug/
marimo/_static/
marimo/_lsp/
__marimo__/

# Project Specific
local_settings.py
data/*.db
data/*.db-shm
data/*.db-wal
204 changes: 203 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,203 @@
# pyconjpbot2
# pyconjpbot2

PyCon JP Slackワークスペース用のチャットボット。slack-machineフレームワークを使用して構築。

## 概要

pyconjpbot2は、既存のpyconjpbotをslack-machine(最新のSlack Pythonフレームワーク)に移植したものです。以下の機能を提供します:

- 🤖 **基本コマンド**: ping, help, version, thx
- ➕ **感謝カウント**: `名前++` / `名前--` で感謝を記録、ランキング表示
- 📖 **カスタム用語辞書**: よく使う用語や情報を登録・検索
- 🌐 **翻訳**: DeepL APIを使用した多言語翻訳
- 🔍 **検索**: Wikipedia、Google検索
- 🔢 **計算**: 数式評価
- 👋 **挨拶・リアクション**: キーワードベースの自動応答
- 🎫 **JIRA統合**: issue検索・表示

## 必要要件

- Python 3.13
- uv (パッケージマネージャー)
- Slack Workspace(Bot Token必須)

## セットアップ

### 1. リポジトリをクローン

```bash
git clone https://github.com/pyconjp/pyconjpbot2.git
cd pyconjpbot2
```

### 2. Python環境のセットアップ

```bash
# uvをインストール(未インストールの場合)
curl -LsSf https://astral.sh/uv/install.sh | sh

# 依存関係をインストール
uv sync --all-extras
```

### 3. 設定ファイルの作成

```bash
# サンプル設定ファイルをコピー
cp local_settings.py.sample local_settings.py

# local_settings.pyを編集して実際の値を設定
# 最低限必要: SLACK_APP_TOKEN, SLACK_BOT_TOKEN
```

### 4. Slack Appの作成

1. [Slack API](https://api.slack.com/apps)にアクセス
2. "Create New App" → "From scratch"
3. App NameとWorkspaceを選択
4. 以下のURLにあるmanifest.yamlを設定してアプリを作成
- https://dondebonair.github.io/slack-machine/user/usage/
5. Basic Information→App-Level Tokensで"Install to Workspace"で `connections:write` のトークン(`xapp-xxx`)を作成し、local_settings.pyのSLACK_APP_TOKENに設定
6. Botをワークスペースにインストール
7. OAuth & PermissionsのBot User OAuth Token(`xoxb-xxx`)をコピーしてlocal_settings.pyのSLACK_BOT_TOKENに設定

### 5. ボットの起動

```bash
# 開発環境
uv run slack-machine

# 本番環境(バックグラウンド実行)
nohup uv run slack-machine > logs/bot.log 2>&1 &
```

## 使用方法

### 基本コマンド

```
@pyconjpbot ping # ボット応答確認
@pyconjpbot help # ヘルプ表示
@pyconjpbot version # バージョン情報
@pyconjpbot thx # 使用技術クレジット
```

### 感謝カウント

```
takanory++ # takanoryに+1
Python-- # Pythonに-1
$plusplus ranking # ランキング表示
$plusplus search 検索語 # 検索
$plusplus delete 名前 # 削除(count < 10のみ)
$plusplus rename 旧 新 # リネーム
$plusplus merge 元 先 # マージ
```

### カスタム用語辞書

```
$term create 酒 # 「酒」コマンドを作成
$酒 add ビール # 応答を追加
$酒 # ランダムに応答を表示
$酒 list # すべての応答を表示
$term drop 酒 # 「酒」コマンドを削除
$term search 検索語 # 用語検索
```

### 翻訳

```
$translate python # 自動検出 → 日本語
$translate -en こんにちは # 日本語 → 英語
```

### 検索

```
$wikipedia Python # Wikipedia検索
$google PyCon JP # Google検索
```

### 計算

```
1 + 1 # 2
sqrt(2) # 1.4142135623730951
```

### JIRA統合

```
SAR-123 # issue情報を表示
$jira search キーワード # Open issueを検索
$jira allsearch 検索語 # すべてのissueを検索
$jira assignee ユーザー名 # 担当issue一覧
```

## 開発

### テスト実行

```bash
# すべてのテストを実行
uv run pytest

# カバレッジレポート生成
uv run pytest --cov=src --cov-report=html
open htmlcov/index.html
```

### コード品質チェック

```bash
# Lintチェック
uv run ruff check src

# フォーマット
uv run ruff format src

# 型チェック
uv run mypy src
```

### テストファースト開発

このプロジェクトはテストファースト原則に従います:

1. **Red**: テストを書く(失敗する)
2. **Green**: 最小限の実装でテストをパス
3. **Refactor**: コードを改善

詳細は[specs/001-slack-bot-migration/tasks.md](specs/001-slack-bot-migration/tasks.md)を参照。

## プロジェクト構造

```
pyconjpbot2/
├── src/
│ └── plugins/ # slack-machineプラグイン
├── tests/
│ ├── unit/ # ユニットテスト
│ ├── integration/ # 統合テスト
│ └── contract/ # コントラクトテスト
├── data/ # データベース
├── specs/ # 仕様書・設計ドキュメント
├── pyproject.toml # プロジェクト設定
└── local_settings.py.sample # 設定ファイルサンプル
```

## ライセンス

MIT License - 詳細は[LICENSE](LICENSE)を参照

## クレジット

- slack-machine: Slack bot framework
- SQLModel: Database ORM
- DeepL API: Translation service
- pytest: Testing framework

---

**PyCon JP Team** | https://www.pycon.jp/
36 changes: 36 additions & 0 deletions local_settings.py.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""
pyconjpbot2 設定ファイルサンプル

このファイルをコピーして local_settings.py を作成し、実際の値を設定してください。
local_settings.py は .gitignore に含まれているため、リポジトリにコミットされません。

使用方法:
1. cp local_settings.py.sample local_settingssettings.py
2. local_settings.py を編集して実際の値を設定
"""

# API Token for Bot integration
# https://api.slack.com/apps/A0386KC9E2V/general: App-Level Tokens
SLACK_APP_TOKEN: str = "<your-app-token>"
# https://api.slack.com/apps/A0386KC9E2V/install-on-team
SLACK_BOT_TOKEN: str = "<your-bot-token>"

# ============================================================================
# External API Configuration (オプション)
# ============================================================================

# DeepL Translation API
# https://www.deepl.com/ja/pro-api
DEEPL_API_KEY: str | None = None # DeepL API Key(翻訳機能を使用する場合)


# Settings for jira plugin
JIRA_URL: str = "https://pyconjp.atlassian.net/"
JIRA_PROJECTS: list[str] = ["ISSHA", "HBI", "HRS"] # JIRA Project Keys
JIRA_DEFAULT_PROJECT: str = "HRS" # JIRA Default Project Key

# see https://docs.google.com/spreadsheets/d/1YiqErBDdp5QWfTlfDmxc6Vi696b_NGFJKzuyM-v6PDM/edit#gid=0
# https://id.atlassian.com/manage/api-tokens でトークンを生成
JIRA_USERNAME: str | None = None
JIRA_USER: str | None = None
JIRA_PASS: str | None = None
63 changes: 63 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
[project]
name = "pyconjpbot2"
version = "0.0.1"
description = "PyCon JP Slack bot using slack-machine framework"
authors = [
{name = "Takanori Suzuki", email = "takanori@takanory.net"}
]
requires-python = "==3.13.*"
readme = "README.md"
license = {file = "LICENSE"}

dependencies = [
"slack-machine>=0.37.0",
"sqlmodel>=0.0.22",
"deepl>=1.18.0",
"wikipedia-api>=0.7.1",
"google-api-python-client>=2.0.0",
"jira>=3.0.0",
"structlog>=25.4.0",
"tox>=4.32.0",
]

[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-cov>=5.0.0",
"pytest-asyncio>=0.24.0",
"ruff>=0.8.0",
"mypy>=1.11.0",
"tox>=4.0.0",
]

[tool.ruff]
target-version = "py313"
line-length = 100

[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
]
ignore = [
"E501", # line too long (handled by formatter)
]

[tool.mypy]
python_version = "3.13"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_any_unimported = false
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
check_untyped_defs = true
strict_equality = true

1 change: 1 addition & 0 deletions src/plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

70 changes: 70 additions & 0 deletions src/plugins/misc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""基本的なボットコマンド(misc)プラグイン

User Story 1: ping, shuffle, choiceコマンドを提供
"""

import random

from machine.plugins.base import MachineBasePlugin
from machine.plugins.decorators import respond_to
from machine.plugins.message import Message
from structlog.stdlib import get_logger

from .utils import get_thread_ts

logger = get_logger(__name__)


class MiscPlugin(MachineBasePlugin):
"""基本的なユーティリティコマンドを提供するプラグイン"""

@respond_to(r"^ping$")
async def ping_command(self, msg: Message) -> None:
"""ボットの動作確認コマンド

コマンド: $ping
応答: pong
"""
logger.info(f"ping_command: user={msg.sender.id}")
await msg.say("pong", thread_ts=get_thread_ts(msg))

@respond_to(r"^shuffle(\s+(?P<words_str>.*)|\s*)$")
async def shuffle_command(self, msg: Message, words_str: str | None) -> None:
"""shuffle: 単語をシャッフルするコマンド

コマンド: $shuffle <単語1> <単語2> ...
応答: シャッフルされた単語のリスト
"""
logger.info(f"shuffle_command: user={msg.sender.id}, input={words_str}")

help_text = "使い方: `$shuffle <単語1> <単語2> ...`"
resp_msg = help_text
if words_str is not None:
words = words_str.split()
if words:
random.shuffle(words)
result = " ".join(words)
logger.info(f"shuffle_command: result={result}")
resp_msg = result

await msg.say(resp_msg, thread_ts=get_thread_ts(msg))

@respond_to(r"^choice(\s+(?P<words_str>.*)|\s*)$")
async def choice_command(self, msg: Message, words_str: str | None) -> None:
"""choice: 単語からランダムに1つ選択するコマンド

コマンド: $choice <単語1> <単語2> ...
応答: ランダムに選ばれた1つの単語
"""
logger.info(f"choice_command: user={msg.sender.id}, input={words_str}")

help_text = "使い方: `$choice <単語1> <単語2> ...`"
resp_msg = help_text
if words_str is not None:
words = words_str.split()
if words:
result = random.choice(words)
logger.info(f"choice_command: result={result}")
resp_msg = f"{result}"

await msg.say(resp_msg, thread_ts=get_thread_ts(msg))
Loading