3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[fondesk用bot]SlashCommandで表記ゆれに抗う

Last updated at Posted at 2020-03-18

はじめに

皆さんリモートワークしていますか?慣れてきましたか?
前回の記事で弊社がfondeskを導入し、メンションをつけてくれるbotを開発しましたよというお話をしました。(無事にfondeskは正式採用されたようです。)

今回は、メンションをつけるために必要になる、表記ゆれ対策用の辞書を更新するSlash Commandを開発した話です。

関連ツール&技術

今回紹介する機能の関連項目です。

  • Slack
  • AWS
    • API Gateway
    • Lambda
    • S3
  • Serverless Framework
  • Python3.7

事の発端

前回紹介のbotを導入してから、良い感じにメンションがついているのを喜んでいたのですが、(見ないふりをしていた)事態が勃発しました。そう、表記ゆれです。。

田が「た」なのか「だ」なのか、漢字で投稿されるのかカタカナで投稿されるのか等々…少しずれた表記で投稿されたことで飛ぶ@hereメンション...この段階では、Lambdaの中に直に辞書を持っており、修正後にはLambdaをデプロイし直さなければいけませんでした。なので発見したら辞書に追加してデプロイ!を都度やっていました。。。

つらい

何度かはfondesk用チャンネルで@hereが出ると飛んでいき、辞書を修正して、デプロイ!とやっていたのですが、、、
思ったより、揺れてました。主だった人はすでに登録してたのですが、全員分やるのだるい…(あと@hereが無闇に飛んでしまうのも心苦しい)ということで、辞書登録を各自に委ねることにしました。(自然言語処理と戦うという選択肢も有りましたが、今回は避けています。)

公式を見てみると「なまえ辞書の使い方」なるものがあり、これである程度は防げているようでした。(漢字で来る投稿は登録済みということなのかな?)

要件

  1. slackから辞書の操作ができる。
  2. 各ユーザーがそれぞれ自分の辞書の[登録, 確認, 削除]ができる。

これでやるなら、Slash Command + API Gateway + Lambdaですな、ということで実装します。

設定&実装

設定

slack側

前回作成したAppsにcommandsを追加しておきます。
Features→Slash Commands→Create New Commandと進み、必要事項を埋めて保存します。
e1Jt1NlCYsc0IF61584523142_1584523165.png

AWS側

今回もServerlessでズドンとやります。
ここで、timeoutを10秒に設定していますが、どのみちslack側は3000msしか待ってくれないので、あまり意味はありません。(将来的に3秒以上処理に時間がかかるようだと、Lambdaの非同期化を考えないといけないですね。)

serverless.yml
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形式に変換しておきます。

slashcommand.py
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の後に打った文字列が格納されています。
今回のケースだと

  1. add
  2. list
  3. remove
    を待ち構えておき、実行する処理を切り替えていきます。

実行する処理側の実装は↓のような感じです。基本的にはS3から読み込んで、JSONファイルの中身を変更し、その後S3にアップロードするという流れです。

Commands.py
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

スクリーンショット 2020-03-18 18.09.28.png スクリーンショット 2020-03-18 18.09.01.png ### Add スクリーンショット 2020-03-18 18.10.08.png スクリーンショット 2020-03-18 18.10.19.png ### Remove スクリーンショット 2020-03-18 18.10.45.png スクリーンショット 2020-03-18 18.10.53.png

まとめ

今回はSlash Commandを使って、fondeskメンション用の辞書の更新ができるようにしました。これで一人で表記ゆれと格闘する日々は終わり、作業が各自に委ねられることで安心してfondeskチャンネルを眺められるようになりました。めでたし、めでたし。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?