なんとなくAPI gatewayとLambdaを使ってみたかったので、遊びとしてServerless Frameworkを使ってLINE botを作った。
どんなbotかと言うと
- 画像を送るとgoogle cloud vision APIを使って文章を検出
- 検出された文章をgoogle cloud translate APIを使って日本語に翻訳して返す
準備
awsのアカウントとかaws cliとかの準備、設定は割愛。
google cloud visionとかのgoogle cloud translateの使い方とかも割愛。
Serverless Frameworkインストール
$ npm install -g serverless
$ serverless --version
1.26.1
serviceの作成
今回はpython3を使います。
$ serverless create --template aws-python3 --name translate_bot --path translate_bot
$ cd translate_bot
Serverless Frameworkのプラグインインストール
今回、google cloud visionやgoogle cloud translateのパッケージの中でAmazon linuxの環境でコンパイルしないといけないモジュールが一部あった(具体的には grpcio
)ので、その解決方法としてserverless-python-requirementsを使います。
$ npm init
$ npm install --save serverless-python-requirements
pythonの必要パッケージのインストール
$ pip install requests google-cloud-vision google-cloud-translate line-bot-sdk
LINE botのコードを書く
import os
import json
import hmac
import base64
import hashlib
import requests
import logging.config
from google.cloud import vision
from google.cloud import translate
from google.cloud.vision import types
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import MessageEvent, TextMessage, TextSendMessage
# load Line API setting
with open('./conf/line.json', 'r') as f:
line_conf = json.loads(f.read())
# set env
os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = './conf/google_service.json'
# logging setting
logging.config.fileConfig('./conf/logging.conf')
logger = logging.getLogger()
logger.setLevel(10)
def translate_from_image(url):
vision_client = vision.ImageAnnotatorClient()
translate_client = translate.Client()
target_language = 'ja'
headers = {'Content-Type': 'application/json',
'Authorization': 'Bearer ' + line_conf.get('channelAccessToken')}
try:
res = requests.get(url, headers=headers)
except:
error_message = 'Failed to fetch the image resource: ' + str(url)
logger.error(error_message)
return (False, 500, error_message)
content = res.content
image = types.Image(content=content)
try:
res = vision_client.text_detection(image=image)
except:
error_message = 'Failed to access to google cloud vision API'
logger.error(error_message)
return (False, 500, error_message)
text = res.full_text_annotation.text
if not text:
return (True, 200, 'Can not detect any texts from the image')
try:
translated_text = translate_client.translate(text, target_language=target_language).get('translatedText')
return (True, 200, translated_text)
except:
error_message = 'Failed to access to google cloud translate API'
logger.error(error_message)
return (False, 500, error_message)
def lambda_handler(request, context):
body = request.get('body', '')
headers = request.get('headers', '')
channel_access_token = line_conf.get('channelAccessToken')
channel_secret = line_conf.get('channelSecret')
line_bot_api = LineBotApi(channel_access_token)
handler = WebhookHandler(channel_secret)
hash = hmac.new(channel_secret.encode('utf-8'), body.encode('utf-8'), hashlib.sha256).digest()
signature = base64.b64encode(hash).decode('utf-8')
if signature != headers.get('X-Line-Signature'):
logger.info('Invalid header: X-Line-Signature: ' + headers.get('X-Line-Signature'))
return {'statusCode': 400, 'body': '{}'}
event = json.loads(body).get('events')[0]
message_type = event.get('message').get('type')
message_id = event.get('message').get('id')
reply_token = event.get('replyToken')
image_url = 'https://api.line.me/v2/bot/message/' + str(message_id) + '/content'
if message_type != 'image':
logger.info('The message type is not image')
return {'statusCode': 400, 'body': '{}'}
response = translate_from_image(image_url)
ret = response[0]
status = response[1]
message = response[2]
if ret:
try:
line_bot_api.reply_message(reply_token, TextSendMessage(message))
return {'statusCode': status, 'body': '{}'}
except:
error_message = 'Can not reply to a talk channel'
logging.error(error_message)
return {'statusCode': 500, 'body': '{}'}
else:
return {'statusCode': status, 'body': '{}'}
上記のコードではLINEとgoogle APIを使用するためのAPI keyが書かれたファイルを下記の通り作る必要がある。
- LINE API key
{
"channelAccessToken": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
"channelSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
- GCPサービスアカウントキー
{
"type": "service_account",
"project_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"private_key_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"private_key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"client_email": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"client_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"auth_uri": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"token_uri": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"auth_provider_x509_cert_url": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"client_x509_cert_url": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
自分で作っておいてですが、このシークレットの管理の仕方はものすごくダメですね。
コードと一緒にこれらがs3にアップロードされてしまうので、万が一bucketのアクセス公開範囲とか権限周りを間違ってたらと思うとちょっと危ない。
ssmのparameter storeとかlambdaの環境変数で管理する方がいいと思います。
各APIの使い方とかは公式ドキュメントさんに丸投げします。
- LINE developers
- google cloud vision
- google cloud translate
デプロイ
デプロイに必要なファイルを作成
$ vim serverless.yml
service: translateBot
provider:
name: aws
runtime: python3.6
functions:
hello:
handler: handler.lambda_handler
events:
- http:
path: woobhook
method: post
plugins:
- serverless-python-requirements
custom:
pythonRequirements:
dockerizePip: non-linux
$ pip freeze > requirements.txt
$ cat requirements.txt
cachetools==2.0.1
certifi==2018.1.18
chardet==3.0.4
future==0.16.0
google-api-core==1.1.0
google-auth==1.4.1
google-cloud-core==0.28.1
google-cloud-translate==1.3.1
google-cloud-vision==0.30.1
googleapis-common-protos==1.5.3
grpcio==1.10.0
idna==2.6
line-bot-sdk==1.5.0
protobuf==3.5.2.post1
pyasn1==0.4.2
pyasn1-modules==0.2.1
pytz==2018.3
requests==2.18.4
rsa==3.4.2
six==1.11.0
urllib3==1.22
準備が整ったのでデプロイします!
リージョンはap-northeast-1(東京)を指定します。
$ serverless deploy -r ap-northeast-1
Serverless: Installing required Python packages with python3.6...
Serverless: Docker Image: lambci/lambda:build-python3.6
Serverless: Linking required Python packages...
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Unlinking required Python packages...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (20.82 MB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
........................
Serverless: Stack update finished...
Service Information
service: translateBot
stage: dev
region: ap-northeast-1
stack: translateBot-dev
api keys:
None
endpoints:
POST - https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/dev/webhook
functions:
hello: translateBot-dev-hello
AWSのコンソールからも新しいAPI gatewayのエンドポイントとLambda関数が作成されたことが確認できます。
また、上記の出力の中のendpointsの値をwebhookのURLとしてLINE developersのコンソールから設定する。
ここまでできたら動くはず。
試しに画像を送ってみる。
これはAWSのブログの一部のスクリーンショット
これはネットで拾った適当なロシア語の文章
うまく動いてるようです。