はじめに
皆さんリモートワークしていますか?慣れてきましたか?
前回の記事で弊社がfondeskを導入し、メンションをつけてくれるbotを開発しましたよというお話をしました。(無事にfondeskは正式採用されたようです。)
今回は、メンションをつけるために必要になる、表記ゆれ対策用の辞書を更新するSlash Commandを開発した話です。
関連ツール&技術
今回紹介する機能の関連項目です。
- Slack
- AWS
- API Gateway
- Lambda
- S3
- Serverless Framework
- Python3.7
事の発端
前回紹介のbotを導入してから、良い感じにメンションがついているのを喜んでいたのですが、(見ないふりをしていた)事態が勃発しました。そう、表記ゆれです。。
田が「た」なのか「だ」なのか、漢字で投稿されるのかカタカナで投稿されるのか等々…少しずれた表記で投稿されたことで飛ぶ@here
メンション...この段階では、Lambdaの中に直に辞書を持っており、修正後にはLambdaをデプロイし直さなければいけませんでした。なので発見したら辞書に追加してデプロイ!を都度やっていました。。。
つらい
何度かはfondesk用チャンネルで@here
が出ると飛んでいき、辞書を修正して、デプロイ!とやっていたのですが、、、
思ったより、揺れてました。主だった人はすでに登録してたのですが、全員分やるのだるい…(あと@here
が無闇に飛んでしまうのも心苦しい)ということで、辞書登録を各自に委ねることにしました。(自然言語処理と戦うという選択肢も有りましたが、今回は避けています。)
公式を見てみると「なまえ辞書の使い方」なるものがあり、これである程度は防げているようでした。(漢字で来る投稿は登録済みということなのかな?)
要件
- slackから辞書の操作ができる。
- 各ユーザーがそれぞれ自分の辞書の[登録, 確認, 削除]ができる。
これでやるなら、Slash Command + API Gateway + Lambdaですな、ということで実装します。
設定&実装
設定
slack側
前回作成したAppsにcommandsを追加しておきます。
Features→Slash Commands→Create New Commandと進み、必要事項を埋めて保存します。
AWS側
今回もServerlessでズドンとやります。
ここで、timeoutを10秒に設定していますが、どのみちslack側は3000msしか待ってくれないので、あまり意味はありません。(将来的に3秒以上処理に時間がかかるようだと、Lambdaの非同期化を考えないといけないですね。)
slash-command:
handler: slashcommand.handler
name: fondesker-slashcommand-${self:provider.stage}
timeout: 10
events:
- http:
path: fondesker/slashcommand
method: post
integration: lambda
request:
passThrough: WHEN_NO_TEMPLATES
template:
application/octet-stream:
'{"headers":{
#foreach($key in $input.params().header.keySet())
"$key": "$input.params().header.get($key)"#if($foreach.hasNext),#end
#end
},
"body": "$util.base64Encode($input.json(''$''))"
}'
application/x-www-form-urlencoded:
'{"body": $input.json(''$'')}'
response:
headers:
Content-Type: "'application/json'"
template: $input.path('$')
statusCodes:
200:
pattern: ''
401:
pattern: '.*"statusCode": 401,.*'
template: $input.path("$.errorMessage")
headers:
Content-Type: "'application/json'"
environment:
FONDESK_BOT_ID: ${env:FONDESK_BOT_ID}
SLACK_TOKEN: ${self:custom.token.${self:provider.stage}}
S3BUCKET: ${self:custom.S3Bucket.name}
S3KEY: ${self:custom.S3Bucket.key}
deploy成功後に表示されるAPI Gatewayのエンドポイントを控えておき、Slack側に記入しておきます。
実装
Slash Command側からはapplication/x-www-form-urlencoded
形式でリクエストが飛んできます。
parse_qsでDict形式に変換しておきます。
import os
import json
import re
from typing import List, Dict, Any
from urllib.parse import parse_qs
import requests
from utils.Commands import Commands
HELP = """
このスラッシュコマンドは次の3つのコマンドが実行できます。\n
1. /fondesker list\n
2. /fondesker add <name> <NAME> (スペース区切りで複数指定可)\n
3. /fondesker remove <name> <NAME> (スペース区切りで複数指定可)
"""
def handler(event: Dict[str, str], context: Any) -> str:
body = event["body"]
params = parse_qs(body)
user_id = params["user_id"][0]
# 引数がなかった場合
if params.get("text") is None:
return HELP
args = re.split(r"\s", params["text"][0])
commands = Commands(bucket=os.environ["S3BUCKET"], key=os.environ["S3KEY"])
if args[0] == "add":
res = commands.add_list(user_id, args[1:])
elif args[0] == "list":
res = commands.get_list(user_id)
elif args[0] == "remove":
res = commands.remove_list(user_id, args[1:])
else:
res = HELP
if type(res) is list:
res = list2str(res)
return res
def list2str(res: List[str]) -> str:
if len(res) == 0:
response_sentence = "現在登録されていません。"
else:
response_sentence = "現在名簿に登録されている名前は、\n"
response_sentence += " ".join(res)
response_sentence += "\nです。"
return response_sentence
textキー以下に、/commandの後に打った文字列が格納されています。
今回のケースだと
- add
- list
- remove
を待ち構えておき、実行する処理を切り替えていきます。
実行する処理側の実装は↓のような感じです。基本的にはS3から読み込んで、JSONファイルの中身を変更し、その後S3にアップロードするという流れです。
import os
import json
from io import BytesIO
from typing import List
from dataclasses import dataclass
from utils.s3_bucket import S3Bucket
@dataclass
class Commands:
"""
Slash Commandで実行したい処理を書く。
名簿はS3に格納してある想定なので、随時S3へのアクセス権限は付与しておく。
params:
bucket: 名簿が保存されているバケット名
key: 名簿へのS3 Key
region: S3バケットのリージョン(default: ap-northeast-1)
"""
bucket: str
key: str
region: str = "ap-northeast-1"
def __post_init__(self,):
self.s3_bucket = S3Bucket(self.region, self.bucket)
def _get_name_list(self,):
return json.load(self.s3_bucket.get_object_body(self.key))
def get_list(self, user_id: str) -> List[str]:
name_list = self._get_name_list()
return name_list.get(user_id, [])
def add_list(self, user_id: str, names: List[str]) -> List[str]:
name_list = self._get_name_list()
if user_id in name_list.keys():
name_list[user_id].extend(names)
name_list[user_id] = list(set(name_list[user_id]))
else:
name_list[user_id] = names
self.s3_bucket.put_object(key=self.key, body=json.dumps(name_list), content_type="text/json")
return name_list[user_id]
def remove_list(self, user_id: str, names: List[str]) -> List[str]:
name_list = self._get_name_list()
if user_id in name_list.keys():
name_list[user_id] = [synonym for synonym in name_list[user_id] if synonym not in names]
self.s3_bucket.put_object(key=self.key, body=json.dumps(name_list), content_type="text/json")
return name_list.get(user_id, [])
結果
List
### Add ### Removeまとめ
今回はSlash Commandを使って、fondeskメンション用の辞書の更新ができるようにしました。これで一人で表記ゆれと格闘する日々は終わり、作業が各自に委ねられることで安心してfondeskチャンネルを眺められるようになりました。めでたし、めでたし。