【GAS/LINE】同時複数ユーザー相手にキャッシュ管理!
こんにちは、luthです
ノンプログラマーですが、仕事でも、
GASで軽いツール(ほぼオモチャ)をいくつか導入して、
面倒をどんどんなくそうと日々奮闘中です
さて、今回も家庭内でもオモチャを導入しようと、
お手軽導入のGAS × LINEでbot作りに励む中、
壁にブチ当たったキャッシュのお話です
問題点
GASでは
CacheService.getScriptCache()
や、
CacheService.getUserCache()
などでキャッシュが用意できますが、
ことLINE向けで考えるとデプロイ時にこんな設定にしますよね…?
そう、「ウェブアプリとしての実行者」
=「LINEからくるpostの処理を実行する人」は
デプロイする開発者自身になるのです…!
おお、これじゃbotに同時多数ユーザーがLINEしたら、
キャッシュでモード管理や、情報保持ができないぞ!
趣旨
今回考えた解決策はとてもシンプルです
「LINEユーザーごとに別のキャッシュを用意する」
…それがどうやるのかって話ですね、ええ
ミソ
・LINE UserIdから個人識別用変数を生成
・各キャッシュラベルに個人識別用変数を付与
こうすればキャッシュがお手軽に個人ごとに分けられますね!
AさんとBさんが別々のモードに入りたがってる時でも、
GAS側でキャッシュ管理をゴチャゴチャにせず、
それぞれ別の動きを持たせられます!
サンプルコード
では、サンプルコードです
独自のキャッシュクラス
まずはキャッシュ操作そのものの関数
キャッシュクリアを扱いやすいようにクラス化しています
class MyCache {
constructor(cache_name) {
this.cache_name = cache_name;
//自力でスクリプトで使ったキャッシュラベルを収集しておく
this.keys = [
'type', //投稿者状態 モード切替に利用
'who', //投稿者が誰かをキャッシュにしておく
'gokigen', //ごきげん具合
];
//ここの配列の中に利用しているキャッシュラベルを「cache_name」をつけて記述することで、個人ごとにキャッシュクリアできるようにする
this.all_caches = this.keys.map((key, index, array) => {
return this.cache_name + key;
});
this.cache = CacheService.getScriptCache();
}
/**
* キャッシュクリア
* removeAll()相当
*
* @param - なし
* @return - なし
*/
cacheClear() {
this.cache.removeAll(this.all_caches);
}
/**
* 全キャッシュを取得し、連想配列で、指定のキャッシュをすべて返す
* getAll()相当
*
* @param - なし
* @return {Object} - 連想配列で、指定のキャッシュをすべて返す
*/
log() {
return this.cache.getAll(this.all_caches);
}
/**
* オリジナルのputメソッド
* 個人識別用の文字列を付与している
*
* @param {string} key - キャッシュの検索キー、ラベル
* @param {any} value - キャッシュしたい内容 文字列として保存される
* @param {number} expirationInSeconds - キャッシュ保持したい秒数 省略時は10分、制限は1秒~6時間(21600秒)
* @return - なし
*/
put_individual(key, value, expirationInSeconds) {
key = this.cache_name + key;
if (expirationInSeconds !== undefined) {
expirationInSeconds = Math.floor(Number(expirationInSeconds)); //小数点あったら切り捨て
if (expirationInSeconds >= 1 && expirationInSeconds <= 21600) {
this.cache.put(key, value, expirationInSeconds);
} else { //制限外なら最大6時間を指定
this.cache.put(key, value, 21600);
}
} else { //省略時
this.cache.put(key, value);
}
}
/**
* オリジナルのgetメソッド
* 個人識別用の文字列を付与している
*
* @param {string} key - キャッシュの検索キー
* @return {string} cache_data - キャッシュされていたデータ
*/
get_individual(key) {
key = this.cache_name + key;
return this.cache.get(key);
}
/**
* オリジナルのremoveメソッド
* 個人識別用の文字列を付与している
*
* @param {string} key - キャッシュの検索キー
* @return - なし
*/
remove_individual(key) {
key = this.cache_name + key;
this.cache.remove(key);
}
}
個人識別用変数の生成
続いて、個人識別用の変数を作る関数
バリバリに個人情報っぽいものを入れてますが、擬似個人情報につき悪しからず。
リストをGASに直接記述しましたが、スプレッドシートで管理する方がGASでの追加・削除もできるので良さそうですね
/**
* LINE ユーザIDを引数に取り、名前などに変換する
*
* @param {string} uid - LINE UserID(表出ししていないID)
* @return {Object} x - 変換した名前など。
*/
function convertUid(uid) {
var x = {};
var list = [
//疑似個人情報DBより作成(https://hogehoge.tk/personal/)
{ 'name': '横溝 千夏', 'id': 'qqNJnXhWaTQXxsv4tIN44PlY3iaCjrzc9', 'cache_name': 'cy ' },
{ 'name': '西沢 柚季', 'id': 'UvOTcTbPkPSbzXTMzFmt51cv7RO4Ff1fI', 'cache_name': 'yn ' },
{ 'name': '橘美 智子', 'id': 'O0rusS4gnXv5SiB7Jl1SxdMSZnY6DFCIW', 'cache_name': 'mt ' },
{ 'name': '木内 玲子', 'id': 'sDhf4G6gpYNNJE0Kxiv4CEr7Zh2plNEQL', 'cache_name': 'rk ' },
{ 'name': '島田 武蔵', 'id': 'MSTKC2ESenuVx2V523UwB0XstSfVabczL', 'cache_name': 'ms ' },
{ 'name': '荻原 沙和', 'id': 'ASXHamMOolkYpP1VKcfQ1L5D8n3Wvhjjh', 'cache_name': 'so ' },
{ 'name': '田代 泰知', 'id': 'tuyO7rxwiAzt7YZIOq8q0SgUWYS9bPs0n', 'cache_name': 'tt ' },
{ 'name': '森井 彩音', 'id': 'sUe8LeKk7bECG62LWuUB1BcLt1lfOnjVq', 'cache_name': 'am ' },
{ 'name': '滋賀 友一', 'id': 'JCOpyBrR0la3ashA8dDAwtgwSP1gfCjhi', 'cache_name': 'ts ' },
{ 'name': '森永 華蓮', 'id': '4fG8r6i19HtiTcaGosumubxchAgaOVWgB', 'cache_name': 'km ' },
{ 'name': 'LINE Dev', 'id': 'Udeadbeefdeadbeefdeadbeefdeadbeef', 'cache_name': 'line ' }, //LINEが検証時にUserIDに載せてくる値
];
for (var i = 0; i < list.length; i++) {
if (list[i].id == uid) {
x.name = list[i].name;
x.cache_name = list[i].cache_name;
cache.put(list[i].cache_name + 'who', list[i].name);
break;
}
}
return x;
}
利用例
そして、キャッシュを利用したLINE bot例です
初期状態:「ご機嫌いかが?」とご機嫌を伺う
1段階目(type=0):今日の夕ご飯を訊く
2段階目(type=1):ご機嫌とご飯をまとめる
「やめる」:キャッシュクリア
/**
* メイン関数、LINEからのPOSTを受け取り、リターン
*/
function doPost(e) {
var access_token = { /* LINE アクセストークン */ }
var data = JSON.parse(e.postData.contents);
var url = 'https://api.line.me/v2/bot/message/reply';
var lineType = data.events[0].type;
var reply_token = data.events[0].replyToken;
//リプライトークンがない、フォローイベントはスルー
if (typeof reply_token === undefined || lineType === 'follow' || lineType === 'unfollow') {
return;
}
var uid = data.events[0].source.userId;
var cache_name = convertUid(uid).cache_name;
var myCache = new MyCache(cache_name);
var message = data.events[0].message.text;
var type = myCache.get_individual('type');
if (type == undefined) {
var who = myCache.get_individual('who');
var text = who + 'さん、こんにちは\nごきげんいかが?';
myCache.put_individual('type', '0');
} else {
if (message === 'やめる') {
var text = 'さようなら';
myCache.cacheClear();
} else {
switch (type) {
case '0':
myCache.put_individual('type', '1');
myCache.put_individual('gokigen', message);
var text = message + 'ですか、それはいいですね';
text += '\n今日の夜ご飯は?'
break;
case '1':
var gokigen = myCache.get_individual('gokigen');
var text = message + 'とは、' + gokigen + 'って日にちょうどいいですね!';
myCache.cacheClear();
break;
}
}
}
var headers = {
"Content-Type": "application/json; charset=UTF-8",
'Authorization': 'Bearer ' + access_token,
};
var postData = {
"replyToken": reply_token,
"messages": [
{
'type': 'text',
'text': text,
}
]
};
var options = {
"method": "post",
"headers": headers,
"payload": JSON.stringify(postData)
};
UrlFetchApp.fetch(url, options);
return ContentService.createTextOutput(JSON.stringify({ 'content': 'post ok' })).setMimeType(ContentService.MimeType.JSON);
}
以上となります
これで自分以外の人にも、自作botを安心して渡せますね…!