不審者情報をマップ付きで通知してくれるLINE BotをAWSで構築した

#はじめに
私は生まれてからずっと東京近辺に住んでいるのですが、1日1回は挙動のおかしい人を見かけるんですよね。
変な人から身を守るためには、事前に情報を入手することが必要です。
ということで、今回は警視庁が発表している不審者情報をLINEで通知してくれるBotを作成しました。

元ネタは以下の記事です。
[東京23区の不審者情報をプッシュしてくれるLINE Botつくってみた｜立花翔｜note](https://note.mu/stachibana/n/nd03db61687fb)

今回の開発ではテキスト情報にプラスして、住所からマップ画像を生成して通知できるようにしました。

#友達登録
![husin.png](https://qiita-image-store.s3.amazonaws.com/0/104983/3191f0cd-a4d5-d87e-ae30-e5166022ff7b.png)

![Zp7yCyhv-z.png](https://qiita-image-store.s3.amazonaws.com/0/104983/cb8faac9-eae0-af94-789e-1975ce4a3cac.png)
スマホの方は[こちら](https://line.me/R/ti/p/Zp7yCyhv-z)

#使い方
アカウントを登録し、通知対象のエリアを登録します。
![IMG_3560.jpg](https://qiita-image-store.s3.amazonaws.com/0/104983/1a24fbc4-9756-f4e8-1a96-20ff6fb357b7.jpeg)

その後毎日1回、通知対象エリアの不審者情報がある場合は、情報が配信されます。
![IMG_3558.jpg](https://qiita-image-store.s3.amazonaws.com/0/104983/46e9d4e4-234f-4dd8-d030-c89b53909fee.jpeg)

ちなみにこのケースだと、おじさんは道を尋ねて見事不審者データベース入りを果たしました:cop_tone1:

#構成
![LINE不審者.jpg](https://qiita-image-store.s3.amazonaws.com/0/104983/f09380ea-5a88-0a09-955e-38497696810b.jpeg)

まずLINEのBotアカウントをフォローすると、Webhook URLに設定したAPI Gatewayにリクエストが送られます。それはLambdaで受け取り、ユーザIDをDynamo DBに登録します。
その後ユーザより通知対象エリア（「足立区」「あきる野市」など）をテキストで送信してもらい、同様の流れでエリアをDB登録します。
あとは配信用のLambda関数を定期実行させて、通知対象エリアと一致するユーザに対して、Push APIで不審者情報とマップ画像を送信します。

#手順
①Dynamo DBのテーブル作成
「ユーザID」と「通知対象エリア」を値として持つテーブルを作成します。
(プライマリキーはユーザID)

②Lambda関数の作成
言語はPython3を使用しました。

```python:登録
import urllib.request
import json
import boto3
from boto3.dynamodb.conditions import Key

DYNAMO_DB = boto3.resource('dynamodb')
LINE_CHANNEL_ACCESS_TOKEN =''

def message(event):
    DYNAMO_DB.Table('SuspiciousPersonInfoUser').update_item(
        Key={
            'user_id': event['source']['userId']
        },  
        UpdateExpression='set area=:area',
        ExpressionAttributeValues={
            ':area': event['message']['text']
        }
    )
    
    REPLY_URL = 'https://api.line.me/v2/bot/message/reply'
    headers = {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + LINE_CHANNEL_ACCESS_TOKEN
    }
    body = {
        'replyToken': event['replyToken'],
        'messages': [
            {
                'type': 'text',
                'text': event['message']['text'] + 'を登録しました。'
            }
        ]
    }
    
    req = urllib.request.Request(REPLY_URL, data=json.dumps(body).encode('utf-8'), method='POST', headers=headers)
    urllib.request.urlopen(req)
        
def follow(event):
    DYNAMO_DB.Table('SuspiciousPersonInfoUser').put_item(
        Item={
            'user_id': event['source']['userId']
        }   
    )
    
def unfollow(event):
    DYNAMO_DB.Table('SuspiciousPersonInfoUser').delete_item(
        Key={
            'user_id': event['source']['userId']
        }
    )

def lambda_handler(request, context):
    for event in request['events']:
        if event['type'] == 'message':
            message(event)
        elif event['type'] == 'follow':
            follow(event)
        elif event['type'] == 'unfollow':
            unfollow(event)  
    return {'statusCode': 200, 'body': '{}'}
```

```python:通知
import urllib.request
import urllib.parse
import datetime
import json
import boto3
from boto3.dynamodb.conditions import Key
from bs4 import BeautifulSoup

def push_message(user_id_list,encode_place,message):
    MULTICAST_URL = 'https://api.line.me/v2/bot/message/multicast'
    LINE_CHANNEL_ACCESS_TOKEN = ''
    GOOGLE_MAP_API_KEY = ''
    map_url = 'https://maps.google.com/maps/api/staticmap?center=' + encode_place + '&zoom=15&size=250x250&&scale=2&key=' + GOOGLE_MAP_API_KEY
    headers = {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + LINE_CHANNEL_ACCESS_TOKEN
    }
    data = {
        'to' : user_id_list,
        'messages' : [
            {
                'type' : 'text',
                'text' : message
            },
            {
                'type' : 'image',
                'originalContentUrl' : map_url,
                'previewImageUrl' : map_url
            }
        ]
    }

    request = urllib.request.Request(MULTICAST_URL, data=json.dumps(data).encode('utf-8'),method='POST', headers=headers)
    urllib.request.urlopen(request)

def main():
    day = datetime.date.today().day

    try:
        req = urllib.request.urlopen('http://www.keishicho.metro.tokyo.jp/kurashi/higai/kodomo/fushin/' + str(day) + '_fushin.html')
    except urllib.error.HTTPError as e:
        print('Error Code:' + str(e.code))
        return

    soup = BeautifulSoup(req,'html.parser')
    table = soup.find('table')
    td_list = table.find_all('td')
    th_list = table.find_all('th')

    for i,td in enumerate(td_list):
        place = td.text.split('、')[2].split('の')[0].split('付近')[0]
        encode_place = urllib.parse.quote(place)
        user_id_list = []

        DYNAMO_DB = boto3.resource('dynamodb')
        response = DYNAMO_DB.Table('SuspiciousPersonInfoUser').query(
            IndexName='area-index',
            KeyConditionExpression=Key('area').eq(th_list[i].text)
        )
        items = response['Items']
        
        for item in items:
            user_id_list.append(item['user_id'])

        message = str(td.find('p'))[3:-4].replace('<br/>','\r\n')

        if user_id_list:
            push_message(user_id_list,encode_place,message)

def lambda_handler(event, context):
    main()
```

DynamoDBへの読み書きが発生しますので、IAMで権限を設定する必要があります。

③API Gatewayの設定
Lambda関数をPOSTメソッドの送信先に設定します。
（登録用Lambda関数のARNを設定）

④LINE Messaging APIの設定
LINE Developerより、Webhook URLにAPI Gatewayで生成したURLを設定します。
![hushin_developer.png](https://qiita-image-store.s3.amazonaws.com/0/104983/b076f399-899b-213e-5c7a-9fa89b7132d7.png)

⑤CloudWatch Eventsで定期実行の設定
毎日12:10に通知用Lambda関数が実行されるようにcron式を設定する。

fin.

#おわりに
このBotがあれば、東京へお出かけする時も安心です:relaxed:
あっ、ちなみに不審者情報は平日にしか更新されませんので、休日出かける際は役に立ちませんのでご注意ください:sweat_smile:

少しでも「面白い！」「参考になる！」と思いましたら、いいねボタンをクリックしていただけると幸いです:bow_tone1: