Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ISSHA-2537 create new bot #1

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,6 @@ dmypy.json

# Pyre type checker
.pyre/

# sqlite db
*.db
109 changes: 108 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,109 @@
# pyconjpbot2
# 🤖 pyconjpbot2

Slack bot for PyCon JP Slack

## ▶️ Commands

### [calc.py](/plugins/calc.py)

- 数式を計算する / Calculate formulas

```
takanory: 1 + 1
pyconjpbot: 2
takanory: sqrt(2)
pyconjpbot: 1.4142135623730951
```

### [greeting.py](/plugins/greeting.py)

- あいさつを返す / Return a greeting message

```
takanory: おはよう
pyconjpbot: @takanory おはようございます
```

### [translate.py](/plugins/trasnlate.py)

- テキストを翻訳する / Translate text
- `$translate python`: 指定した文字列を日本語に翻訳する
- `$translate へび`: 指定した文字列を英語に翻訳する
- `$translate -DE へび` : 指定した言語(DE等)に翻訳する
- `$translate list`: 指定できる言語の一覧を返す

### [plusplus.py](/plugins/plusplus.py)

- 指定された名前の++をカウントする / Count ++ for a given name
- `name1 name2++`: 指定された名前に +1 カウントする
- `name1 name2--`: 指定された名前に -1 カウントする
- `$plusplus search (keyword)`: 名前にkeywordを含む一覧を返す
- `$plusplus delete (name)`: 指定されたnameのカウントを削除する(カウント10未満のみ)
- `$plusplus rename (old) (new)`: カウントする名前をnewに変更する
- `$plusplus merge (old) (new)`: 2つの名前のカウントをnewにまとめ、oldを削除する

### [misc.py](/plugins/misc.py)

- `$choice spam ham eggs`: 指定された単語から1つをランダムに選んで返す
- `$shuffle spam ham eggs`: 指定された単語をシャッフルした結果を返す
- `$ping`: 応答(pong)を返す
- `$version`: バージョン情報を返す
- `$random`: チャンネルにいるメンバーからランダムに一人を選ぶ
- `$random active`: チャンネルにいるactiveなメンバーからランダムに一人を選ぶ

### [wikipedia.py](/plugins/wikipedia.py)

- 指定されたキーワードに関連するWikipediaのページを返す / Return Wikipedia page for specified keywords and language
- `$wikipedia keywords`: Wikipediaで指定されたキーワードに関連するページを返す
- `$wikipedia -en keywords`: Wikipediaで指定された言語(en等)のページを返す

### [reaction.py](/plugins/reaction.py)

- 任意のキーワードでemojiリアクションを追加する / Add emoji reactions for any keywords

## 🔧 How to build

```bash
$ python3.10 -m venv env
$ . env/bin/activate
(env) $ pip install -r requirements.txt
(env) $ cp example.env .env
(env) $ vi .env
(env) $ python app.py
```

## ✨ Lint, Mypy

* `tox -e lintcheck`: check black, isort and flake8
* `tox -e mypy`: check mypy

```bash
$ python3.10 -m venv env
$ . env/bin/activate
(env) $ pip install -r requirements-dev.txt
(env) $ tox -e lintcheck
...
lintcheck run-test: commands[0] | isort -c --diff app.py
lintcheck run-test: commands[1] | black --check app.py
...
lintcheck run-test: commands[2] | flake8 app.py
___________________________________ summary ____________________________________
lintcheck: commands succeeded
congratulations :)
(env) $ tox-e mypy
...
mypy run-test: commands[0] | mypy app.py
...
___________________________________ summary ____________________________________
mypy: commands succeeded
congratulations :)
```

## 📚 References

* [Bolt for Python](https://slack.dev/bolt-python/tutorial/getting-started)
* [slack_bolt API documentation](https://slack.dev/bolt-python/api-docs/slack_bolt/)
* [Python Slack SDK](https://slack.dev/python-slack-sdk/)
* [slack_sdk API documentation](https://slack.dev/python-slack-sdk/api-docs/slack_sdk/)
* [Intro to Socket Mode](https://api.slack.com/apis/connections/socket)
* [Web API methods](https://api.slack.com/methods)
31 changes: 31 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import logging
import os
from typing import Any

from dotenv import load_dotenv
from slack_bolt import App, Say
from slack_bolt.adapter.socket_mode import SocketModeHandler

from plugins import enable_plugins

# take environment variables from .env
load_dotenv()

logging.basicConfig(level=logging.DEBUG)

# Initializes app with bot token
app = App(token=os.environ["SLACK_BOT_TOKEN"])


@app.message("hello")
def message_hello(message: dict[str, Any], say: Say) -> None:
# say() sends a message to the channel where the event was triggered
say(f"Hey there <@{message['user']}>!")


enable_plugins(app)


# Start pycon jp bot
if __name__ == "__main__":
SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"]).start()
19 changes: 19 additions & 0 deletions example.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Settings for Bolt Python
# https://api.slack.com/apps
# "Basic Information" -> "App-Level Tokens" ->
SLACK_APP_TOKEN=xapp-...
# "OAuth & Permissions" -> "Bot User OAuth Token"
SLACK_BOT_TOKEN=xoxb-...

# Settings for plugins/translate.py: Sign up DeepL API for free
# https://www.deepl.com/pro-api?cta=header-pro-api/
DEEPL_AUTH_KEY=...

# Settings for plugins/jira.py
JIRA_URL=https://pyconjp.atlassian.net/
# JIRA Default Project Key
JIRA_DEFAULT_PROJECT=TRA
# Generate API token with https://id.atlassian.com/manage/api-tokens
# see https://docs.google.com/spreadsheets/d/1YiqErBDdp5QWfTlfDmxc6Vi696b_NGFJKzuyM-v6PDM/edit#gid=0&range=48:48
JIRA_USER=...
JIRA_TOKEN=...
10 changes: 10 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[mypy]
python_version = 3.10
disallow_incomplete_defs = True
show_column_numbers = True
show_error_context = True
ignore_missing_imports = True
follow_imports = skip
check_untyped_defs = True
warn_unused_ignores = True
strict_optional = False
14 changes: 14 additions & 0 deletions plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from slack_bolt import App

from . import calc, greeting, jira, misc, plusplus, reaction, translate, wikipedia


def enable_plugins(app: App) -> None:
calc.enable_plugin(app)
translate.enable_plugin(app)
wikipedia.enable_plugin(app)
plusplus.enable_plugin(app)
jira.enable_plugin(app)
greeting.enable_plugin(app)
reaction.enable_plugin(app)
misc.enable_plugin(app)
45 changes: 45 additions & 0 deletions plugins/calc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""
Calculate formulas
"""

import logging
from re import compile

from slack_bolt import App, Say
from sympy import SympifyError, sympify

# single number pattern
NUM_PATTERN = compile(r"^\s*[-+]?[\d.,]+\s*$")

logger = logging.getLogger(__name__)


def enable_plugin(app: App) -> None:
@app.message(compile(r"^(([-+*/^%!(),.\d\s]|pi|e|sqrt|sin|cos|tan|log)+)$"))
def calc(message: dict, say: Say) -> None:
"""
Calculate a string like a formula and return the result
"""
logger.info("excecute calc function")
formula = message["text"]
formula = formula.replace(",", "")
# ignore single number
if NUM_PATTERN.match(formula):
return

try:
result = sympify(formula)
except SympifyError:
# ignore not a formula
return

if result.is_Integer:
answer = f"{int(result):,}"
else:
try:
answer = f"{float(result):,}"
except SympifyError:
# ignore result is not a number
return

say(answer, thread_ts=message.get("thread_ts"))
63 changes: 63 additions & 0 deletions plugins/greeting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""
Return a greeting message
"""

import logging
import random
from re import compile

from slack_bolt import App, Say

logger = logging.getLogger(__name__)


def _send_greeting(message: dict, say: Say, greetings: tuple[str, ...]) -> None:
greeting = random.choice(greetings)
say(f"<@{message['user']}> {greeting}", thread_ts=message.get("thread_ts"))


def enable_plugin(app: App) -> None:
@app.message(compile(r"おはよう|お早う"))
def morning(message: dict, say: Say) -> None:
"""Return morning greeting"""
logger.info("excecute morning function")
greetings = (
"おはよう",
"おはよー",
"おはようございます",
)
_send_greeting(message, say, greetings)

@app.message(compile(r"こんにち[はわ]"))
def hello(message: dict, say: Say) -> None:
"""Return hello greeting"""
logger.info("excecute hello function")
greetings = (
"こんにちは",
"ちーっす",
"こんにちは、元気ですかー?",
)
_send_greeting(message, say, greetings)

@app.message(compile(r"いってきま|行ってきま"))
def see_you(message: dict, say: Say) -> None:
"""Return see you greeting"""
logger.info("excecute see_you function")
greetings = (
"いってらっしゃい",
"いってらっしゃーい",
"いってらっしゃ~い",
"いってら",
)
_send_greeting(message, say, greetings)

@app.message(compile(r"眠た?い|ねむた?い|寝る|寝ます"))
def night(message: dict, say: Say) -> None:
"""Return night greeting"""
logger.info("excecute night function")
greetings = (
"おやすみなさい",
"おやす",
"おやすー",
)
_send_greeting(message, say, greetings)
63 changes: 63 additions & 0 deletions plugins/jira.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""
jira
"""

import logging
import os
from re import compile

from dotenv import load_dotenv
from jira import JIRA, JIRAError
from slack_bolt import App, Say
from slack_sdk.models.blocks import SectionBlock

# take environment variables from .env
load_dotenv()

# Clean JIRA Url to not have trailing / if exists
CLEAN_JIRA_URL = os.environ["JIRA_URL"].removesuffix("/")

# Login to jira
jira_auth = (os.environ["JIRA_USER"], os.environ["JIRA_TOKEN"])
jira = JIRA(CLEAN_JIRA_URL, basic_auth=jira_auth)

projects = jira.projects()
project_keys = [prj.key for prj in projects]
issue_pattern = compile(rf"({'|'.join(project_keys)})-\d+")

logger = logging.getLogger(__name__)


def _create_issue_blocks(issue_id: str) -> list[SectionBlock]:
"""Create blocks for issue information"""
issue = jira.issue(issue_id)

summary = issue.fields.summary
if issue.fields.assignee:
assignee = issue.fields.assignee.displayName
else:
assignee = "未割り当て"
status = issue.fields.status.name
issue_url = issue.permalink()

blocks = [
SectionBlock(
text=f"*<{issue_url}|{issue_id} {summary}>*",
fields=[f"担当者: {assignee}", f"ステータス: {status}"],
),
]
return blocks


def enable_plugin(app: App) -> None:
@app.message(issue_pattern)
def jira_issue(message: dict, say: Say) -> None:
"""Return issue information"""
logger.info("execute jira_issue function")
# find all issue id in message
for m in issue_pattern.finditer(message["text"]):
try:
blocks = _create_issue_blocks(m[0])
say(blocks=blocks, thread_ts=message.get("thread_ts"))
except JIRAError:
pass
Loading