#概要
前回の投稿において対話型LINE BOTの基本機能を実装できたので、応用編としてリプライ機能の向上を図っていく。今回作成するのはテキストメッセージの代わりに画像メッセージをユーザへリプライする機能である。一例として監視カメラの画像をLINEを通じてオンデマンドに表示させるソリューションを実装していきたい。
##やりたいこと
自宅の猫の様子を監視カメラで撮影し、LINEアプリを通じてウォッチしたい。
##メッセージ仕様
LINE Messaging APIリファレンス - 画像メッセージ
によるとBOTはLINEメッセージ内で画像ファイルを直接やりとりするのではなく、インターネットのリンク情報を通知するものらしい。
つまりメッセージ本文中には画像のバイナリデータは一切含まれず、LINEアプリが改めてURL先からデータを直接取得してくるという仕組みのようだ。こうすることによってLINEとしてはデータ中継の手間がなくなりトラフィック削減に寄与できるとともに、ユーザにとっては遅延が少なくなるためユーザ満足度も向上するという双方のメリットを考慮しているものと想定される。よって、画像ファイルはインターネットに対してReachableな場所に配置する必要があることがわかる。
##システム構成
自宅に設置した自作の監視カメラで監視対象物(猫)を動体検知により撮影し、WEBサーバにアップロードする。これはBOTのプログラムとは関係なくカメラ側のシステム起因で定期的に行うこととする。
一方、BOTはLINEアプリからの要求(①)に応じてユーザへ画像ファイルのURLを通知(②)し、LINEアプリが画像をダウンロード(③④)する。
自宅内のネットワークとLINE BOTを直接連動させないのはセキュリティの観点からの配慮である。そのため厳密にはリアルタイムの監視ではなく撮影後しばらく経過した画像が表示される仕様となる。
#1.カメラユーザ作成
# useradd camera
# passwd camera
# mkdir camera/upload
# chown camera:camera camera/upload
# chmod 700 camera/upload
#2.FTP設定
$ sudo -s
# yum -y install vsftpd
# 12行目:匿名ログイン禁止
anonymous_enable=NO
# 82,83行目:コメント解除 (アスキーモードでの転送を許可)
ascii_upload_enable=YES
ascii_download_enable=YES
# 100,101行目:コメント解除 (chroot有効)
chroot_local_user=YES
chroot_list_enable=YES
# 103行目:コメント解除 (chroot リストファイル指定)
chroot_list_file=/etc/vsftpd/chroot_list
# 109行目:コメント解除 (ディレクトリごと一括での転送有効)
ls_recurse_enable=YES
# 114行目:変更 (IPv4をリスンする)
listen=YES
# 123行目:変更 (もし不要なら IPv6 はリスンしない)
listen_ipv6=NO
# ルートディレクトリ指定 (指定しない場合はホームディレクトリがルートディレクトリとなる)
local_root=public_html
# ローカルタイムを使う
use_localtime=YES
# seccomp filter をオフにする (ログインに失敗する場合はオフにする)
seccomp_sandbox=NO
#パッシブモードの設定
pasv_enable=YES
pasv_min_port=0
pasv_max_port=0
chrootしない(ローカルディレクトリより上層へのcdを許可するユーザ)リストを作成する
#空のファイルでもいいので存在している必要がある
ftpログインを禁止するユーザを指定
#必要に応じて追記
# service vsftpd start
vsftpd 用の vsftpd を起動中: [ OK ]
# sudo chkconfig vsftpd on
# chkconfig --list | grep ftp
vsftpd 0:off 1:off 2:on 3:on 4:on 5:on 6:off
###iptablesの設定
アクティブモードだとクライアント側のセキュリティソフト等で弾かれるためパッシブモードの設定を行う必要がある
# iptables -A SERVICES -p tcp --dport 21 -j ACCEPT
IPTABLES_MODULES="ip_conntrack_ftp ip_nat_ftp"
引用:「ip_conntrack_ftp」モジュールはFTPプロトコルを識別し、使うポートを追跡して適宜開けてくれます。(つまり、ポートレベルでフィルタリングするのではなく、Windowsファイアウォールなどと同じようにアプリケーションレベルでフィルタリングしてくれます。)
また、IPマスカレードを使用している場合は、「ip_nat_ftp」モジュールもロードします。
便利なモジュールがあるんですね。
# service vsftpd restart
iptables: チェインをポリシー ACCEPT へ設定中filter [ OK ]
iptables: ファイアウォールルールを消去中: [ OK ]
iptables: モジュールを取り外し中: [ OK ]
iptables: ファイアウォールルールを適用中: [ OK ]
iptables: 追加のモジュールを読み込み中:ip_conntrack_ftp ip_[ OK ]
#3.監視カメラ作成
試験的に簡易な方法で監視カメラを作成する。
用いるもの
- PC(
windows
) - USBカメラ
- キャプチャソフト(
LiveCapture3
)
##キャプチャソフトのFTP設定
今回採用したLiveCapture3は、USBカメラなど様々なデバイスに対応しており、静止画や動画をキャプチャしてメール送信、FTP送信できるほか、RTP配信にも対応している高機能フリーウェアである。
指定したタイミング(周期)での定期撮影もできるほか、動体検知により指定したFTPサーバへ画像をアップロードする機能まで、フリーウェアでありながらこれらの機能を全て単体で有しており、今回のニーズに完璧に合致する神ツールである。感謝。
- 動作モード: 動体検知
- アクション: 静止画をFTPにアップロード
- 転送モード: PASV
上記設定でFTPサーバに画像が送信されることを確認。
#4.画像ファイルのリサイズ
LINE Messaging APIリファレンス - 画像メッセージ
によると画像ファイルの要件は下記の通り。
- originalContentUrl
- JPEG
- 最大画像サイズ:4096×4096
- 最大ファイルサイズ:1MB
- previewImageUrl
- JPEG
- 最大画像サイズ:240×240
- 最大ファイルサイズ:1MB
これらに適合させるべくサーバ側で画像ファイルのリサイズ処理を行う。
ImageMagickというツールが便利な模様。
# yum -y install ImageMagick ImageMagick-devel
# identify -verbose a.jpg
Image: a.jpg
Format: JPEG (Joint Photographic Experts Group JFIF format)
Class: DirectClass
Geometry: 1280x720+0+0
Resolution: 96x96
Print size: 13.3333x7.5
convertコマンドの主なオプション
-resize
「横×縦」の形式で指定すれば、その枠に収まるようにアスペクト比固定でリサイズ
-auto-orient
画像のexif情報を元に自動回転
-strip
画像のexif情報を除去
下記のリサイズ案を採用する。
# convert -strip -resize 896x504 source_file 閲覧用
# convert -strip -resize 179x100 source_file プレビュー用
また公開ファイルへの第三者による無作為アクセス対策としてハッシュ値を算出しファイル名に付加しておく。こうすることによってURLの類推を防止できる。
# md5sum source_file
以上を踏まえ、公開用ディレクトリに画像を保存(オーバーライド)するスクリプトを記述する。
#/!bin/bash
CP='/bin/cp'
RM='/bin/rm'
CONV='/usr/bin/convert'
date=`date '+%Y%m%d%H%M%S'`
SRC_DIR='/home/home/upload' #カメラ画像保存ディレクトリ
DST_DIR='/usr/share/nginx/html/media' #公開ディレクトリ
LATEST_DIR=`ls -rt ${SRC_DIR} | tail -n 1`
LATEST_FILE=`ls -rt ${SRC_DIR}/${LATEST_DIR} | tail -n 1`
LATEST_FILE_PATH=${SRC_DIR}/${LATEST_DIR}/${LATEST_FILE}
MD5=($(md5sum ${LATEST_FILE_PATH}))
DST_FILE_PATH=${DST_DIR}/${LATEST_FILE%.*}_org_${MD5}.${LATEST_FILE##*.}
DST_FILE_PREV=${DST_DIR}/${LATEST_FILE%.*}_prev_${MD5}.${LATEST_FILE##*.}
${RM} -r ${DST_DIR}/*
${CP} ${LATEST_FILE_PATH} ${DST_FILE_PATH}
${CONV} -strip -resize 896x504 ${DST_FILE_PATH} ${DST_FILE_PATH}
${CONV} -strip -resize 179x100 ${DST_FILE_PATH} ${DST_FILE_PREV}
このシェルをcronに登録して繰り返し周期的に実行し続ける。するとSRC_DIRに格納されている画像ファイルの中から常に最新のファイルが選択されて、DST_DIRの公開用ファイルを上書きし続ける。つまり最後に動体検知した瞬間の静止画(=最新の猫の状態)がLINEに通知される仕組みが出来上がる。
# crontab -e
* * * * * /home/camera/script/camera.sh 1> /dev/null
#5.LINE BOTに画像メッセージリプライ機能を追加
前回作成したBOTに画像メッセージ機能をアドオンする。
ユーザから送信するメッセージの中に特定のワード(起動コード)が含まれていた場合に条件分岐し、対話APIに投げるのではなく画像メッセージをリプライするように仕向ける。
今回設定した起動コードは以下の4パターンである。いずれかがユーザのメッセージ文中に含まれていた場合、(多少強引であるが)会話の文脈に関係なく処理を分岐させるようにする。
CATVIEW_KEY = ['カメラ','室内','部屋','猫']
またレスポンスにも、画像メッセージと併せてランダムなコメントを挿入させてみる。例えば以下のようなものである。
CATVIEW_RES = ['カメラの画像を送るよ',
'猫ちゃんの様子です',
'自宅警備中です',
'写真で確認よろしく']
以下に出来上がったコード全体を示す。
def action_res
def response_image
の関数を新規作成し、line_handlerの#action部で起動コードの判定を行っている。
# -*- Coding: utf-8 -*-
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse
from django.shortcuts import render
from datetime import datetime
from time import sleep
import requests
import json
import base64
import logging
import os
import random
import log.logconfig
from utils import tools
logger = logging.getLogger('commonLogging')
LINE_ENDPOINT = 'https://api.line.me/v2/bot/message/reply'
LINE_ACCESS_TOKEN = '' #Line developer管理画面で確認
LINE_ALLOW_USER='' #Line developer管理画面で確認
ULOCAL_ENDPOINT = 'https://chatbot-api.userlocal.jp/api/chat'
ULOCAL_API_KEY = '' #Userlocal登録時に確認
IMAGE_DIR = '/usr/share/nginx/html/media'
IMAGE_URL = 'https://mydomain.com/media'
CATVIEW_KEY = ['カメラ','室内','部屋','猫']
CATVIEW_RES = ['カメラの画像を送るよ',
'猫ちゃんの様子です',
'自宅警備中です',
'写真で確認よろしく']
@csrf_exempt
def line_handler(request):
#exception
if not request.method == 'POST':
return HttpResponse(status=200)
logger.debug('line_handler message incoming') #logging
out_log = tools.outputLog_line_request(request) #logging
request_json = json.loads(request.body.decode('utf-8'))
for event in request_json['events']:
reply_token = event['replyToken']
message_type = event['message']['type']
user_id = event['source']['userId']
#whitelist
if not user_id == LINE_ALLOW_USER:
logger.warning('invalid userID:' + user_id) #logging
return HttpResponse(status=200)
#action
if message_type == 'text':
if any(s in event['message']['text'] for s in CATVIEW_KEY):
action_res(reply_token,'catview')
else:
#ulocal chat
response_text(reply_token,ulocal_chatting(event))
return HttpResponse(status=200)
def action_res(reply_token,command):
if command == 'catview':
#監視カメラ画像
files = os.listdir(IMAGE_DIR)
#logger.info(files) #logging
orgFiles = [ s for s in files if '_view' in s ]
orgPATH = IMAGE_DIR + '/' + orgFiles[0]
orgUrl = IMAGE_URL + '/' + orgFiles[0]
subtext = random.choice(CATVIEW_RES)
sleep(1)
response_image(reply_token,orgUrl,orgUrl,subtext)
def response_image(reply_token,orgUrl,preUrl,text):
payload = {
"replyToken": reply_token,
"messages":[
{
"type": 'text',
"text": text
},
{
"type": 'image',
"originalContentUrl": orgUrl,
"previewImageUrl": preUrl
}
]
}
line_post(payload)
def response_text(reply_token,text):
payload = {
"replyToken": reply_token,
"messages":[
{
"type": 'text',
"text": text
}
]
}
line_post(payload)
def line_post(payload):
url = LINE_ENDPOINT
header = {
"Content-Type": "application/json",
"Authorization": "Bearer " + LINE_ACCESS_TOKEN
}
requests.post(url, headers=header, data=json.dumps(payload))
out_log = tools.outputLog_line_response(payload) #logging
logger.debug('line_handler message -->reply') #logging
def ulocal_chatting(event):
url = ULOCAL_ENDPOINT
payload={
'key' : ULOCAL_API_KEY,
'message': event['message']['text']
}
out_log = tools.outputLog_ulocal_request(payload) #logging
logger.debug('ulocal_chatting send request') #logging
ulocal_res = requests.get(url,payload)
logger.debug('ulocal_chatting -->recv response') #logging
out_log = tools.outputLog_ulocal_response(ulocal_res) #logging
data = ulocal_res.json()
response = data['result']
return response
(botenv) [line_bot]$ gunicorn --bind 127.0.0.1:8000 line_bot.wsgi:application
LINEアプリから起動コードを含むメッセージを投下してみると応答が得られることがわかる。
リプライコメントはランダムに選択されるため会話が微妙に噛み合わない感じになるが、この馬鹿っぽさがなんとも憎めない。
今回はここまで。