コンセプト
AWS Lambdaを使用したLINEmessagingAPI使用方法を、
外部連携に重点を置いて解説します。
ソースは極力関数化をし、図やサンプルコード・リンクをなるべく多く使うようにしているので、
初心者にもとっかかりやすい内容になっていると思います。
全体像
LINEからAPI Gatewayをエンドポイントとし、SlackやGoogle Apps Script(GAS)に連携する。
GASにはGoogle Driveへの画像アップロード、Slackにはテキスト通知を担当してもらいます。
LINEアカウント作成
まず、LINE Developersアカウントを作成してください。
作成手順はこちら
※ここで取得した以下の内容をメモに残しておく
・アクセストークン
・Channel Secret
LambdaとAPI Gatewayの作成
API Gatewayについて、今回は特段設定する必要はありません。
詳しい解説はこちら
以下の手順に従って、順々に作成・設定してください。
2.関数の作成画面で関数名を入力し、「関数の作成」をクリック
※ランタイムはPythonの最新版を選択
4.トリガーの設定で「新規APIの作成」を選択し、セキュリティを「オープン」に設定
5.右上の「保存」をクリック
※ここで取得した以下の内容をメモに残しておく
・API Gatewayの「API エンドポイント」
LINEアカウントの設定
再び、LINE Developersのサイトにアクセスし、WebHookの設定をする。
設定項目 | 設定値 |
---|---|
Webhook送信 | 利用する |
Webhook URL | API Gatewayの「API エンドポイント」 ※1 |
自動応答メッセージ | 利用しない |
※1.HTTPS のポート番号を指定して設定してください。 |
https://APIGatewayのドメイン名:443/ステージ名/関数名
Lambdaの環境変数設定
Lambdaのコンソール画面に戻り、以下の設定を行います。
※大切なキー情報の流出を防ぐためにも、環境変数に定義しましょう
キー | 値 |
---|---|
LINE_CHANNEL_ACCESS_TOKEN | アクセストークン |
LINE_CHANNEL_SECRET | Channel Secret |
Pythonコード作成
以下を順々に行ないます。
・LINEからのリクエストであることの検証
・リクエストtypeの判別
・LINEへのリプライ
・Slack通知
・Google Apps Scriptの呼び出し
LINEからのリクエストであることの検証
公式に書かれていること
署名を検証する
検証の手順は以下のとおりです。
1.チャネルシークレットを秘密鍵として、HMAC-SHA256アルゴリズムを使用してリクエストボディのダイジェスト値を取得します。
2.ダイジェスト値をBase64エンコードした値とリクエストヘッダーにある署名が一致することを確認します。
図で見て理解しましょう。
WebHookのHeader情報に含まれる「X-Line-Signature」は以下の手順で作られているため、
同じ手順でハッシュ値を求め、突合すれば検証がうまくいきます。
まずは関数の定義
# 署名の検証
def validateReq(request):
# 検証結果
validateResult = False
try:
# Request情報取得
body = request['body']
header = request['headers']
# リクエストBodyのハッシュ化(SHA256)
hash = hmac.new(LINE_CHANNEL_SECRET.encode('utf-8'),
body.encode('utf-8'), hashlib.sha256).digest()
# エンコーディング(base64)
signature = base64.b64encode(hash).decode('utf-8')
#検証
if signature == header['X-Line-Signature'] :
validateResult = True
except:
logger.info(e.args)
validateResult = False
finally:
return validateResult
lambda_handlerからの呼び出し
def lambda_handler(request, context):
if not validateReq(request) :
logger.info("LINE 以外からのアクセス")
return {'statusCode': 405, 'body': '{}'}
return {'statusCode': 200, 'body': '{}'}
リクエストtypeの判別
今回は、LINEからのメッセージがtextか画像かの判定のみ行います。
※その他のリクエストtypeについては公式を参照
{
"destination": "xxxxxxxxxx",
"events": [
{
"replyToken": "0f3779fba3b349968c5d07db31eab56f",
"type": "message",
"timestamp": 1462629479859,
"source": {
"type": "user",
"userId": "U4af4980629..."
},
"message": {
"id": "325708",
"type": "text",
"text": "Hello, world"
}
},
{
"replyToken": "8cf9239d56244f4197887e939187e19e",
"type": "follow",
"timestamp": 1462629479859,
"source": {
"type": "user",
"userId": "U4af4980629..."
}
}
]
}
例から、複数のeventsオブジェクトのtypeを判別していけば良いとわかります。
コードは以下
#eventの回数分繰り返す
for event in json.loads(request['body'])['events']:
#typeがtextの場合
if event["message"]["type"] == "text":
logger.info("テキストが送られた")
#typeがimageの場合
elif event["message"]["type"] == "image":
logger.info("画像が送られた")
return {'statusCode': 200, 'body': '{}'}
###LINEへのリプライ
以下の関数で、LINEから来たtext情報をおうむ返しできます。
# LINEへリプライ
def replyLine(event):
#リプライ先のURL
url = 'https://api.line.me/v2/bot/message/reply'
#POST-Heder作成
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + LINE_CHANNEL_ACCESS_TOKEN
}
#POST-Body作成
body = {
'replyToken': event['replyToken'],
'messages': [
{
"type": "text",
"text": event['message']['text']
}
]
}
#Requestsオブジェクト生成
req = urllib.request.Request(url, data=json.dumps(body).encode('utf-8'), method='POST', headers=headers)
#LINEへリプライ
with urllib.request.urlopen(req) as res:
logger.info(res.read().decode("utf-8"))
Slack通知
Slackのチャンネルに通知を行います。
Slack側の設定はここが一番わかりやすいです。
簡単なtextを通知するサンプルコードは以下
# Slack通知
def notifySlack(text):
#設定したWebhookのURLを設定してください。
url = "Slack_WebhookのURL"
#POSTデータ作成
send_data = {
"username": "LINE_BOT",
"icon_emoji": ":man-heart-man:",
"text": text
}
send_text = "payload=" + json.dumps(send_data)
#Requestsオブジェクト生成
req = urllib.request.Request(url,data=send_text.encode('utf-8'),method="POST")
#Slack通知
with urllib.request.urlopen(req) as res:
logger.info(res().decode("utf-8"))
POSTリクエストを送るところは、LINEへのリプライ処理と同一のため、共通化しても良いですね!
###Google Apps Scriptの呼び出し
GASの呼び出しを行い、LINEから送られて来た画像をGoogle Driveに保存します。
設定に関してはここを参照してください。
####Script環境変数の設定
Scriptの編集ページから、
ファイル > プロジェクトのプロパティを押下し、スクリプトのプロパティから以下を設定してください。
プロパティ | 値 |
---|---|
GOOGLE_DRIVE_FOLDER_ID | Google Driveの保存先のフォルダID ※1 |
LINE_CHANNEL_ACCESS_TOKEN | LINEのアクセストークン |
※1.フォルダIDはGoogle DriveのフォルダURLの以下の部分です。
https://drive.google.com/drive/u/0/folders/フォルダID?ths=true
####Scriptの作成
LINEからImageIDをPOSTメソッドで受け取り、
ImageIDをキーに画像をBlob型で取得します。
取得した画像データは、Google Driveに保存します。
※POSTメソッドを受け取る場合は、doPostメソッドを定義してあげましょう。
// 環境変数取得
var PROPERTIES = PropertiesService.getScriptProperties();
//Google DriveのフォルダIDの取得
var GOOGLE_DRIVE_FOLDER_ID = PROPERTIES.getProperty('GOOGLE_DRIVE_FOLDER_ID')
//LINEアクセストークンの取得
var LINE_ACCESS_TOKEN = PROPERTIES.getProperty('LINE_CHANNEL_ACCESS_TOKEN')
//LINEからPOSTリクエストを受けたときに起動する
function doPost(e){
//messageIdから、Line上に存在するバイナリ形式の画像URLを取得します
var json = JSON.parse(e.postData.contents);
var messageId= json.messageId;
imageBlob = getImageBlobByImageUrl(messageId);
//Blob形式のデータをGoogle Driveにアップロード
imageUrl = saveImageBlobAsPng(imageBlob)
return imageUrl
}
//messageIdから、Line上に存在するバイナリ形式の画像データを取得します
function getImageBlobByImageUrl(messageId){
var url = "https://api.line.me/v2/bot/message/" + messageId + "/content"
var res = UrlFetchApp.fetch(url, {
'headers': {
'Content-Type': 'application/json; charset=UTF-8',
'Authorization': 'Bearer ' + LINE_ACCESS_TOKEN,
},
'method': 'get'
});
var imageBlob = res.getBlob().getAs("image/png").setName("temp.png")
return imageBlob;
}
//Blob形式のデータをGoogle Driveにアップロード
function saveImageBlobAsPng(imageBlob){
try{
var folder = DriveApp.getFolderById(GOOGLE_DRIVE_FOLDER_ID);
var file = folder.createFile(imageBlob);
return file.getUrl()
} catch (e){
Logger.log(e)
}
}
GASの呼び出し
Lambda側で以下の関数を定義してあげましょう。
#LINE画像をGoogle Driveにアップロードする
def postLineImage(messageId):
url = "GASのURL"
body = {
'messageId': messageId
}
headers = {
}
#Requestsオブジェクト生成
req = urllib.request.Request(url, data=json.dumps(body).encode('utf-8'), method='POST', headers=headers)
#GAS呼び出し
with urllib.request.urlopen(req) as res:
logger.info(res.read().decode("utf-8"))
##まとめ
全ての関数の呼び出しをまとめたソースは以下です。
import logging
import os
import urllib.request, urllib.parse
import json
import base64
import hashlib
import hmac
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# グローバル変数
LINE_CHANNEL_ACCESS_TOKEN = os.environ['LINE_CHANNEL_ACCESS_TOKEN']
LINE_CHANNEL_SECRET = os.environ['LINE_CHANNEL_SECRET']
def lambda_handler(request, context):
#リクエストの検証
if not validateReq(request) :
logger.info("LINE 以外からのアクセス")
return {'statusCode': 405, 'body': '{}'}
#eventの回数分繰り返す
for event in json.loads(request['body'])['events']:
#typeがtextの場合
if event["message"]["type"] == "text":
#LINEへリプライ
replyLine(event)
#Slack通知
notifySlack(event['message']['text'])
#typeがimageの場合
elif event["message"]["type"] == "image":
#Google Driveへアップロード
postLineImage(event['message']['id'])
return {'statusCode': 200, 'body': '{}'}