オンライン雑談会の「開催日は今日だよ!」をSlackに自動投稿する仕組みをサーバーレスで作ってみた

雑談会の日程調整に調整さんを使っています。人間が「今日が開催日だよ!」と告知するのがめんどくさくなったので、サーバーレスで自動化してみました。
2020.04.28

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

定期開催しているオンライン雑談会があります。日時の調整に調整さんが大活躍しています。

  1. 月初に「調整さんを作ってね」のリマインダーがSlackに自動投稿される(以前に作成した仕組み)
  2. 人間が調整さんを作成する
  3. 人間がSlackのチャンネルで案内する(URLと締切)
  4. Botが締切日に「今日が締切だよ!」と案内する(以前に作成した仕組み)
  5. 人間が当日に「今日が開催日だよ!」と案内する

そこそこ自動化してきましたが、ツメが甘かったです。5.も自動化できることに気づきました。やりました!!!

全体概要

前回の仕組みを利用し、Lambda内で処理を分けています。

構成概要

Slackのワークフローを作成する(開催日の入力)

ワークフローの新規作成

Slackの左上を選択し、ワークフロービルダーを起動します。

ワークフロービルダーを起動する

適当に名前を付けます。

Slackのワークフローを作成する

人間が起動するためショートカットを選択します。

ショートカットを選択する

チャンネルと短い名前を入力します。

ショートカットを設定する

ワークフローのステップを追加(フォーム入力)

ステップを追加を選択します。

ステップを追加する

フォームを作成します。

フォームを作成する

ワークフローのステップを追加(メッセージ送信)

さらにステップを追加します。今度はメッセージを送信を選択し、保存します。

メッセージを投稿する

ワークフローを公開する

右上の公開するボタンを選択すればOKです!

ワークフローを公開する

Lambdaの変更

対象日をDynamoDBに保存するLambdaを変更する

前回作成したsrc/save_deadline/app.pyを下記に変更します。DynamoDBに保存するときtypeを設けて区別しています。

  • deadline: 調整さんの回答締切日
  • announce: 同期会の開催日

app.py

import boto3
import json
import logging
import os
import re

from datetime import datetime

logger = logging.getLogger()
logger.setLevel(logging.INFO)

dynamodb = boto3.resource('dynamodb')

SLACK_WORKFLOW_USER_DEADLINE = 'reminder_misc_join_201901_workflow'
SLACK_WORKFLOW_USER_ANNOUNCE = '同期会のお知らせ'


def lambda_handler(event, context):
    main(event)
    return {
        'statusCode': 200
    }

def main(event):
    logger.info(json.dumps(event))

    body = json.loads(event['body'])
    logger.info(json.dumps(body))

    if 'username' not in body['event']:
        logger.info('No username.')
        return

    # 「締切」と「開催日」が同日の場合は考慮しない(運用上なし)

    if body['event']['username'] == SLACK_WORKFLOW_USER_DEADLINE:
        # 調整さんの締切とURLを登録する
        deadline_timestamp = parse_timestamp_for_deadline(body['event']['text'])
        url = parse_url_for_deadline(body['event']['text'])
        item = {
            'deadline': deadline_timestamp,
            'type': 'deadline',
            'expiration': deadline_timestamp + 60*60*11,  # 当日11時をDynamoDBのTTL期限とする
            'url': url
        }
        logger.info(f'item for deadline: {json.dumps(item)}')
        put_item(item)
    elif body['event']['username'] == SLACK_WORKFLOW_USER_ANNOUNCE:
        # 開催日を登録する
        announce_timestamp = parse_timestamp_for_announce(body['event']['text'])
        item = {
            'deadline': announce_timestamp,
            'type': 'announce',
            'expiration': announce_timestamp + 60*60*11,  # 当日11時をDynamoDBのTTL期限とする
        }
        logger.info(f'item for announce: {json.dumps(item)}')
        put_item(item)
    else:
        logger.info('No workflow message.')


def parse_timestamp_for_deadline(text):
    pattern = r'.+\n期限は \*(\d{4}/\d{1,2}/\d{1,2})\* です!'
    res = re.match(pattern, text)
    if res:
        # 0時のunixtimeを返す
        return int(datetime.strptime(res.group(1), '%Y/%m/%d').timestamp())
    raise ValueError

def parse_timestamp_for_announce(text):
    pattern = r'同期会の開催日は \*(\d{4}/\d{1,2}/\d{1,2})\* です!'
    res = re.match(pattern, text)
    if res:
        # 0時のunixtimeを返す
        return int(datetime.strptime(res.group(1), '%Y/%m/%d').timestamp())
    raise ValueError

def parse_url_for_deadline(text):
    pattern = r'.+\n.+\n<(.+)>'
    res = re.match(pattern, text)
    if res:
        return res.group(1)
    raise ValueError

def put_item(item):
    table_name = os.environ['REMINDER_TABLE_NAME']
    table = dynamodb.Table(table_name)
    res = table.put_item(Item=item)
    logger.info(res)

告知するLambdaを変更する

前回作成したsrc/notify_deadline_message/app.pyを下記に変更します。typeによって通知文面を変更しています。

app.py

import boto3
import json
import logging
import os
import requests

from botocore.exceptions import ClientError
from datetime import date, datetime

logger = logging.getLogger()
logger.setLevel(logging.INFO)

INCOMMING_WEBHOOK_URL = os.environ['INCOMMING_WEBHOOK_URL']

dynamodb = boto3.resource('dynamodb')


def lambda_handler(event, context):
    today = get_today()
    logger.info(f'today: {today}')

    remind_data = get_remind_data(today)
    logger.info(f'get_remind_data(): {remind_data}')

    if remind_data is None:
        return

    message = create_message(remind_data)
    post_slack(message)


def get_today():
    today = date.today()
    # 今日の0時0分0秒のunixtimeを返す
    return int(datetime(today.year, today.month, today.day).timestamp())


def get_remind_data(deadline):
    table_name = os.environ['REMINDER_TABLE_NAME']
    table = dynamodb.Table(table_name)
    try:
        res = table.get_item(Key={
                'deadline': deadline
            }
        )
    except ClientError as e:
        logger.error(e.response['Error']['Message'])
        return None
    else:
        return res.get('Item', None)


def create_message(remind_data):
    # https://api.slack.com/incoming-webhooks
    # https://api.slack.com/docs/message-formatting
    # https://api.slack.com/docs/messages/builder
    # https://www.webfx.com/tools/emoji-cheat-sheet/
    if remind_data['type'] == 'deadline':
        return {
            'text': '<!here> 今日が締切です!! 記入お願いします!\n',
            'attachments': [
                {
                    'text': remind_data['url']
                }
            ]
        }
    if remind_data['type'] == 'announce':
        return {
            'text': '<!here> 今日が開催日です!!\n',
        }
    raise AttributeError('unsupport type')

def post_slack(message):
    url = f'https://{INCOMMING_WEBHOOK_URL}'

    # http://requests-docs-ja.readthedocs.io/en/latest/user/quickstart/
    try:
        response = requests.post(url, data=json.dumps(message))
    except requests.exceptions.RequestException as e:
        logger.error(e)
    else:
        logger.info(response.status_code)

ビルド&デプロイ

sam build
sam package \
    --output-template-file packaged.yaml \
    --s3-bucket cm-fujii.genki-chouseisan-reminder-deploy-bucket
sam deploy \
    --template-file packaged.yaml \
    --stack-name Chouseisan-Reminder-Stack \
    --capabilities CAPABILITY_NAMED_IAM \
    --no-fail-on-empty-changeset \
    --parameter-overrides ChouseisanNotifySlackUrl=/Slack/INCOMING_WEBHOOK_URL/channel_name/choseisan_reminder

動作確認

1. Slackのワークフローで調整さんのURLと締切日を登録する

調整さんを作成し、締切日とURLをSlackのワークフローで入力します。

調整さんの締切日を登録する

Slakでは下記のメッセージが自動投稿されます。

Slackに調整さんと締切日が投稿される

DynamooDBには下記が格納されました。

DynamoDBの様子

2. 調整さんの入力締切日に通知がくる

調整さんの締切日が通知される

3. 開催日を登録する

開催日をSlackのワークフローで入力します。

開催日を登録する

Slakでは下記のメッセージが自動投稿されます。

開催日が投稿される

DynamooDBには下記が格納されました。

DynamoDBの様子

4. 開催日に通知がくる!!!

開催日が通知される

さいごに

一通りの自動化ができました。よい調整さんライフをお過ごしください!