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

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

雑談会の日程調整に調整さんを使っています。人間が「今日が開催日だよ!」と告知するのがめんどくさくなったので、サーバーレスで自動化してみました。
Clock Icon2020.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: 同期会の開催日
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によって通知文面を変更しています。

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. 開催日に通知がくる!!!

開催日が通知される

さいごに

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

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.