【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を安心して渡せますね…!

この記事が気に入ったらサポートをしてみませんか?