IT業務効率化

PythonとSlackでtipsボットを作る4【受けとったtipsを管理する】

Slack tips bot

はじめに

tipsボットを作り続けてきて4記事目になりました。

この記事でPythonのコードに関しては、tipsボットが完成しました。その作り方を書いていきたいと思います。

tipsの一覧を表示する

「show tips」と命令した時に、tipsの一覧を表示させます。

まずは「show tips」と命令された時に返事をする関数を、plugins.pyに実装します。

@respond_to('show tips')
def show_tips(message):
    channel_id = message.body["channel"]
    tips = read_tips(channel_id)
    message.reply(tips, in_thread=True)

tipsは数十個作られる可能性があり、それがチャンネルに表示されると、みんなコメントを上に流してしまうので、スレッドの中に返事するようにします。

4行目のread_tipsは、別途関数を作成しました。

def read_tips(channel_id):
    """
    Parameters
    ----------
    channel_id : str
    """
    tips_path = "tips/{}.json".format(channel_id)
    is_exists = os.path.exists(tips_path.format(tips_path))
    if not is_exists:
        return "まだtipsはありません。"

    with open(tips_path, "r") as tips_file:
        tips_json = json.load(tips_file)
    tips = json.dumps(tips_json, ensure_ascii=False, indent=2, sort_keys=True)
    return tips

苦労したポイントはjsonデータを整形する方法です。sortする時にlambda式を使ったり、文字列整形するためにお手製の関数を作りかけていたのですが、やめました。

json.dumpsでindentを指定し、sort_keysの引数をTrueに変えてやるだけで、綺麗に文字列として出力されます。ensure_asciiは日本語の文字化けを防いでいます。

tipsをソートする

前回の記事を読むとわかりやすいのですが、tipsは入力時にtip番号を指定する仕様にしました。このため番号は必ず順番にはなりません。ですので、tips一覧を表示するタイミングでtips番号を振り直すことにします。

表示に対して番号を振り直すだけでなく、tipsを保存しているjsonファイルに上書きもします。今のjsonファイルはtips番号が1と12で飛び飛びになっています。

{
 "1": "部長が好きなのはプレモル",
 "12": "欠席連絡は前日12時まで"
}

これを1,2に番号を振り直すために次の関数を実装しました。

def _sort_tips(tips_path):
    """
    This function is used when referring to tips.
    """
    with open(tips_path, "r") as tips_file:
        tips_json = json.load(tips_file)

    sorted_tips = {}
    key = 1

    for tip in tips_json.values():
        sorted_tips[key] = tip
        key += 1
    with open(tips_path, "w") as tips_file:
        json.dump(sorted_tips, tips_file, ensure_ascii=False)

show_tips関数の中に入れておきます。そうすることで、一覧を確認するたびにtipsをソートできます。

実行してみた結果が以下です。

無事に成功しました。tipsを保持しているjsonも上書きされています。

tipを削除する

不要なtipsを削除するときの処理です。命令文は「delete tip<number>」にしようと思います。まずはその命令をキャッチするための関数を作ります。

@respond_to('delete tip(\d*)')
def forget_tip(message, tip_number):
    channel_id = message.body["channel"]
    deleted_tip = delete_tip(channel_id, tip_number)
    if deleted_tip is None:
        message.reply('No tip was found for the number.')
        return
    message.reply('I deleted tip{}: {}'.format(tip_number, deleted_tip))

命令文の中のtip番号を引数として受け取ります。その番号を受け取ってdelete_tipという関数に与えて、tipを削除します。

delete_tipは削除したtipを返り値として返すようにしました。失敗した時にはNoneを返します。そのため、もしNoneが帰ってきた時は、それに相当するtipがないというメッセージをSlackに返します。

delete_tip関数は以下のようにしました。

def delete_tip(channel_id, tip_number):
    """
    Parameters
    ----------
    channel_id : str
    tip_number : integer

    Returns
    -------
    SUCCESS:
        target_tip : str what you wanted to delete
    FAILURE:
        None
    """
    tips_path = "tips/{}.json".format(channel_id)
    is_exists = os.path.exists(tips_path.format(tips_path))
    if not is_exists:
        return "まだtipsはありません。"

    with open(tips_path, "r") as tips_file:
        tips_json = json.load(tips_file)
    try:
        target_tip = tips_json[tip_number]
    except:  # expect KeyError
        return None
    del tips_json[tip_number]
    return target_tip

これで実行したみたいと思います。まず以下のようにtipsが存在しています。

次に2番のtipsを消してみようと思います。

実行は成功したようです。結果を確認してみます。

無事に消えていました。「show tips」と命令した段階で、tipsの番号が振り直されるので上記のような返答になります。

tipをランダムに送信する

最後にtipsを送信するようにします。1つ目の記事でSlackに送信するところは作成したのですが、tipsを読み取ることはまできていません。最後にtipsを読み取り、ランダムで選択してSlackに送信することろまで完成させようと思います。

そこまでいけば、あとはcronか何かで定期実行するだけです。

ただ各チャンネルのtipsを扱う時に、保存するファイル名をチャンネルIDにしています。そのため、1つ目の記事では

slack.chat.post_message("project-飲み会", "今日はさっさと帰って飲みに行こう!",
                        as_user=True)

という形でチャンネル名で送信先を指定していましたが、このチャンネル名からチャンネルIDを取得しなくてはいけません。そのために以下の関数を実装しました。

def get_channel_id_from_name(channel_name):
    channel_id = None  # default
    raw_data = slack.channels.list().body
    channels_info = raw_data["channels"]
    for channel_info in channels_info:
        if channel_name == channel_info["name"]:
            channel_id = channel_info["id"]
            break
    return channel_id

実装の際にはチャンネルIDを取得するため、この記事にお世話になりました。

次にランダムにtipを取得するための処理です。

def choose_randomly_from_tips(channel_id):
    tips_path = "tips/{}.json".format(channel_id)
    is_exists = os.path.exists(tips_path.format(tips_path))
    if not is_exists:
        return "まだtipsはありません。"
    with open(tips_path, "r") as tips_file:
        tips_json = json.load(tips_file)
    return random.choice(list(tips_json.values()))

この2つの関数を用いて、tipを送信します。

def send_tip(channel_name="project-飲み会"):
    channel_id = get_channel_id_from_name(channel_name)
    tip = choose_randomly_from_tips(channel_id)
    slack.chat.post_message(channel_name, tip,
                        as_user=True)

これを3回実行すると以下のようになります。

今は上の2つのtipsしか覚えさせていないのですが、ランダムで出力されています。

これで完成しました。

send_tip関数の引数にチャンネル名を渡す方法ですが、近日それをdigdagを用いて実装する記事を書いてみようと思います。

最後に

結構気をつけたつもりが殴り書きになってしまいました。特にtips_manager.pyの

    tips_path = "tips/{}.json".format(channel_id)
    is_exists = os.path.exists(tips_path.format(tips_path))
    if not is_exists:
        return "まだtipsはありません。"

は、クラス化することで1つにまとめられるので(__init__の中にでも書いて)、時間のある時に修正しようと思います。

読んでいただきありがとうございました。

git hubのソースコード

前回の記事

ABOUT ME
hirayuki
今年で社会人3年目になります。 日々体当たりで仕事を覚えています。 テーマはIT・教育です。 少しでも技術に親しんでもらえるよう、noteで4コマ漫画も書いています。 https://note.mu/hirayuki