なぜLINEボット?
結婚式で当日の写真を共有できるものを作りたい
また、チャットボットで暇な時間に、みんなが遊んでくれればなと思い作ってみました
結婚式以外のイベントなど、色々使えるシーンがあると思いますので、ここにメモしておきます
LINEを使ったチャットボットのアーキテクチャー
LINE Developers
LINE Developersに登録し「Messaging API(ボット)をはじめる」からボットを作っていきます
プロバイダーに自分を選択し、「次のページ」を押下
アイコンや入力項目を埋めていきます
プランは「Developer Trial」にしていましたが、友達の上限が50人で使えない人が出てしまいました。。
(只し、フリープランは制限があり今回作ったボットを再現することはできないみたいです。。)
作成したMessaging APIを設定していきます
- 「アクセストークン」を発行し、「Webhook送信」を利用するに設定します
- 「Webhook URL」はGASとの連携に使いますが、この段階で下に表示されているQRコードからLINEで友達になることができます(これだけでは、やり取りは何もできないみたいですね)
Google Apps Script
サーバーサイドはGASで作りました。理由は設定が簡単なのとスプレッドシートでプログラミングが出来ない人もボットの回答を変更ができるようにしたかったからです
- スプレッドシートを作る
質問 | トータル回数 | ステータス | 写真 | 馴れ初め,出会い,なれそめ | ・・・ |
---|---|---|---|---|---|
新郎回答 | 新婦、呼んでんで〜 | それは、秘密やね | 質問回答 | ||
新婦回答 | 新郎に代わりま〜す | 共通の友人の紹介です! | 質問回答 |
A1セルから上記のようにスプレッドシートを作りました。「トータル回数」や「写真」列はボットを叩いた回数や写真を送ってくれた回数をユーザーごとにカウントするために作りました。
右には質問を好きなだけ追加できるようにし、その列に新郎新婦それぞれの回答を書いて、質問に対しての返信を作れるようにしました。
「ステータス」に関してはコーディングと一緒にあとで説明します
- スクリプトエディタでコーディング
「ツール」→「スクリプトエディタ」を開きコーディングしていきます
var CHANNEL_ACCESS_TOKEN = 'LINE Developersのアクセストークンを記載'
var dialogueUrl = 'https://api.apigw.smt.docomo.ne.jp/dialogue/v1/dialogue?APIKEY=Docomo雑談対話のAPI key'
function doPost(e) {
var reply_token = JSON.parse(e.postData.contents).events[0].replyToken;
var userId = JSON.parse(e.postData.contents).events[0].source.userId;
var groupId = JSON.parse(e.postData.contents).events[0].source.groupId;
var imageID = JSON.parse(e.postData.contents).events[0].message.id;
var messageType = JSON.parse(e.postData.contents).events[0].message.type;
var dt = new Date();
var minutes = dt.getMinutes();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("linebot");
for(var i=2;i<300;i++){
var range_user = sheet.getRange(i, 1);
var checkUser = range_user.getValue();
if(checkUser == userId){
var userRow = i
break;
}else if(!checkUser){
var range_status = sheet.getRange(i, 3);
range_user.setValue(userId);
range_status.setValue(minutes % 2);
var userRow = i
break;
}
}
var response_message = [];
// 画像、動画判定
if(messageType === 'image'){
addCount(sheet,userRow,4)
response_message.push("写真ありがとうございます(≧▽≦)\nみんなの写真はここに上がっています。\n http://xxxxxxx.html.xdomain.jp/album.html");
}else if(messageType === 'video'){
response_message.push("すいません、動画は共有できません(。-人-。) \nみんなの写真はここに上がっています。\n http://xxxxxxxx.html.xdomain.jp/album.html");
}else{
var user_message = JSON.parse(e.postData.contents).events[0].message.text;
//response_message = responseMessage(userId,sheet,userRow,user_message)
response_message = changeResponseMessage(userId,sheet,userRow,user_message)
}
//LINE メッセージ
if(response_message.length == 3){
LINE_API3(reply_token,response_message);
}else if(response_message.length == 2){
LINE_API2(reply_token,response_message);
}else{
LINE_API(reply_token,response_message);
}
// メッセージが画像であれば保存
if(messageType === 'image'){
saveAlbum(reply_token,imageID);
}
}
LINE DevelopersのアクセストークンでLINEと連携、またドコモの雑談対話APIで適当な返信ができるように設定する為にAPI Keyを設定します
//LINE API でリプライする
function LINE_API(reply_token,response_message){
var url = 'https://api.line.me/v2/bot/message/reply';
UrlFetchApp.fetch(url, {
'headers': {
'Content-Type': 'application/json; charset=UTF-8',
'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
},
'method': 'post',
'payload': JSON.stringify({
'replyToken': reply_token,
'messages': [{
'type': 'text',
'text': response_message[0],
}],
}),
});
return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}
function LINE_API2(reply_token,response_message){
var url = 'https://api.line.me/v2/bot/message/reply';
UrlFetchApp.fetch(url, {
'headers': {
'Content-Type': 'application/json; charset=UTF-8',
'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
},
'method': 'post',
'payload': JSON.stringify({
'replyToken': reply_token,
"messages":[
{
"type":"text",
"text": response_message[0]
},
{
"type":"text",
"text": response_message[1]
}
]
}),
});
return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}
function LINE_API3(reply_token,response_message){
var url = 'https://api.line.me/v2/bot/message/reply';
UrlFetchApp.fetch(url, {
'headers': {
'Content-Type': 'application/json; charset=UTF-8',
'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
},
'method': 'post',
'payload': JSON.stringify({
'replyToken': reply_token,
"messages":[
{
"type":"text",
"text": response_message[0]
},
{
"type":"text",
"text": response_message[1]
},
{
"type":"text",
"text": response_message[2]
}
]
}),
});
return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}
doPost.gsでresponse_messageの要素数をみてボットからのレスポンスを1~3個まで可変できるようにしました
// ドコモ雑談会話にメッセージを投げ、気の利いた回答を受け取る関数
function getDialogueMessage(userId, mes) {
var dialogue_options = {
'utt': mes
}
var options = {
'method': 'POST',
'contentType': 'text/json',
'payload': JSON.stringify(dialogue_options)
};
// ここでドコモ雑談会話エンドポイントにメッセージを投げる
var response = UrlFetchApp.fetch(dialogueUrl, options);
// ドコモAIからの回答はJSON形式なのでオブジェクト変換
var content = JSON.parse(response.getContentText());
// ドコモAIから取得した回答部分を呼び出し元に戻す
return content.utt;
}
これはコメントの通りです
function responseMessage(userId,sheet,userRow,user_message) {
var response_message = [];
var totalNumColumn = 2;
addCount(sheet,userRow,totalNumColumn);
loop:
for(var j=5;j<300;j++){
checkKeyword = sheet.getRange(1, j).getValue();
if(!checkKeyword){break;}
var keyword = sheet.getRange(1, j).getValue().split(',');
var groomAnswer = sheet.getRange(2, j).getValue();
var brideAnswer = sheet.getRange(3, j).getValue();
for(var num=0;num<keyword.length;num++){
if(user_message.indexOf(keyword[num]) > -1){
if((minutes % 2) == 0){
response_message.push(groomAnswer);
}else{
response_message.push(brideAnswer);
}
response_message = recall(response_message,sheet,userRow,j);
break loop;
}
}
}
if(response_message.length == 0){
//ドコモ雑談会話
response_message.push(getDialogueMessage(userId, user_message));
}
return response_message
}
function changeResponseMessage(userId,sheet,userRow,user_message) {
var dt = new Date();
var minutes = dt.getMinutes();
var response_message = [];
var userStatus = sheet.getRange(userRow,3).getValue();
//トータル回数の更新
var totalNumColumn = 2;
addCount(sheet,userRow,totalNumColumn);
//ステータスの変更
if(user_message == "チェンジ"){
if(userStatus == 0){
response_message.push(sheet.getRange(2, 3).getValue());
}else{
response_message.push(sheet.getRange(3, 3).getValue());
}
userStatus = (userStatus + 1)%2;
sheet.getRange(userRow,3).setValue(userStatus);
sheet.getRange(userRow,5,1,100).clear();
}else{
loop:
for(var j=5;j<300;j++){
checkKeyword = sheet.getRange(1, j).getValue();
if(!checkKeyword){break;}
var keyword = sheet.getRange(1, j).getValue().split(',');
var groomAnswer = sheet.getRange(2, j).getValue();
var brideAnswer = sheet.getRange(3, j).getValue();
for(var num=0;num<keyword.length;num++){
if(user_message.indexOf(keyword[num]) > -1){
if(userStatus == 0){
response_message.push(groomAnswer);
}else{
response_message.push(brideAnswer);
}
response_message = recall(response_message,sheet,userRow,j);
break loop;
}
}
}
if(response_message.length == 0){
//ドコモ雑談会話
response_message.push(getDialogueMessage(userId, user_message));
}
}
return response_message
}
function recall(response_message,sheet,userRow,Column) {
addCount(sheet,userRow,Column)
if(checkCount(sheet,userRow,Column) == 3){
response_message[0] = "さっきも言ったけど、、" + response_message[0];
}
else if(checkCount(sheet,userRow,Column) >= 4){
var response_message = [];
response_message.push("なんでやねん!!");
sheet.getRange(userRow, Column).clear()
}
return response_message
}
会話のメイン処理がこのスクリプト。スプレッドシートの質問をみてresponse_messageを返しています
質問が設定されていない場合はドコモの雑談対話に飛ばして適当な返信を返すようにしています
また、質問された回数のカウントとチェンジコマンドで新郎と新婦の回答を入れ替える処理を入れています
(スプレッドシートの「ステータス」はここで使ってます)
さらに、同じ質問を何回もされた時に少し返信を変えるように工夫もしました
//質問回数を確認
function checkCount(sheet,userRow,Column) {
var num = sheet.getRange(userRow, Column).getValue()
return num
}
//質問回数を増やす
function addCount(sheet,userRow,Column) {
var num = sheet.getRange(userRow, Column).getValue()
if(!num){
sheet.getRange(userRow, Column).setValue(1);
}else{
sheet.getRange(userRow, Column).setValue(sheet.getRange(userRow, Column).getValue() + 1);
}
}
質問などの回数をカウントするためのfunctionです
//LINE API 画像などを取得
function saveAlbum(reply_token,messageId){
var url = "https://api.line.me/v2/bot/message/"+messageId+"/content";
var response=UrlFetchApp.fetch(url, {
'headers': {
'Content-Type': 'application/json; charset=UTF-8',
'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
},
'method': 'get'
});
//Logger.log(response.body.toString('base64'));
var file1 = DriveApp.getFolderById('画像を保存したいGoogleDriveのフォルダID').createFile(response); // 保存(名前は指定できない)
file1.setName(messageId+'.png');
Logger.log("conoha push");
conohaPush(response);
return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}
////cocnohaへ画像を一時的に送付
function conohaPush(meta){
var img = Utilities.base64Encode(meta.getBlob().getBytes());
//log_db(img);
var url = 'http://xxx.xx.xxx.xx/create_wedding_album.php';
x=UrlFetchApp.fetch(url, {
'headers': {
'Content-Type': 'application/json',
},
'method': 'post',
'payload': JSON.stringify({
'img': img,
}),
});
Logger.log(x)
return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}
ユーザーがLINEボットに送ってくれが画像をGoogleDriveへ保存処理とconohaを経由しxdomainに設定しているアルバムにとばしています
conoha vps
conohaでGASから画像ファイルを受け取ってアルバム用に加工し、xdomainに送ります
<?php
#$result2= $_POST["img"];
$json_string = file_get_contents('php://input');
$obj = json_decode($json_string,true);
$img=$obj['img'];
$result=base64_decode($img);
$fp = fopen('num.txt', 'r');
$line = fgets($fp);
fclose($fp);
$num=intval($line);
$next=$num+1;
echo $next;
exec('echo '.$next.' > ./num.txt');
$fp = fopen('./'.$num.'.jpg', 'wb');
fwrite($fp, $result);
fclose($fp);
exec('python ../rep.py '.$num.'');
exec('python ../ftp.py '.$num.'');
GASから画像ファイルを受け取り、phthonファイルを呼び出す
0
画像ファイル名をナンバリングする為に作成
import os
import glob
import sys
from PIL import Image
#files2 = glob.glob('html/*.jpg')
#for ff in files2:
# os.remove(ff)
argvs = sys.argv
num=argvs[1]
files = glob.glob('/var/www/html/'+num+'.jpg')
for f in files:
xxx=256.0
img = Image.open(f)
print(img.size)
if img.size[0]/xxx> img.size[1]/xxx:
res=int(img.size[0]/xxx)
else:
res=int(img.size[1]/xxx)
if res ==0:
res=1
img_resize = img.resize((int(img.size[0]/res), int(img.size[1]/res)))
ftitle, fext = os.path.splitext(f)
img_resize.save(ftitle + '_s' + fext)
アルバム用にサイズを縮小した画像ファイルを作成
from ftplib import FTP
import glob
import os
import sys
argvs = sys.argv
num=argvs[1]
f=open('ajax.html','w')
f.write('<div class="ajaxRetrun"><h1></h1><input type="text" id="num" value="'+num+'" />')
f.close
ftp = FTP(
"sv2.html.xdomain.ne.jp",
"xxxxxxxx.html.xdomain.jp",
passwd="???????????????"
)
with open('/var/www/html/'+num+'_s.jpg', 'rb') as f:
ftp.storbinary('STOR /wedding/'+num+'_s.jpg', f)
with open('/var/www/html/'+num+'.jpg', 'rb') as f:
ftp.storbinary('STOR /wedding/'+num+'.jpg', f)
with open('ajax.html', 'rb') as f:
ftp.storbinary('STOR /ajax.html', f)
files2 = glob.glob('/var/www/html/'+num+'*.jpg')
for ff in files2:
os.remove(ff)
ftpを実行しxdomainにファイルを送る
xdomain
後日、追記予定